Import Cobalt 19.master.0.194710
Includes the following patches:
https://cobalt-review.googlesource.com/c/cobalt/+/5190
by errong.leng@samsung.com
diff --git a/src/starboard/android/__init__.py b/src/starboard/android/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/starboard/android/__init__.py
diff --git a/src/starboard/android/apk/apk.gyp b/src/starboard/android/apk/apk.gyp
new file mode 100644
index 0000000..26e8f54
--- /dev/null
+++ b/src/starboard/android/apk/apk.gyp
@@ -0,0 +1,32 @@
+# Copyright 2018 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+{
+ 'targets': [
+ {
+ # This target generates a stamp file that the platform deploy action can
+ # depend on to re-run the Gradle build only when any file that's checked
+ # into git within the 'apk' directory changes.
+ 'target_name': 'apk_sources',
+ 'type': 'none',
+ 'actions': [
+ {
+ 'action_name': 'apk_sources',
+ 'inputs': [ '<!@(git -C <(DEPTH)/starboard/android/apk ls-files)' ],
+ 'outputs': [ '<(PRODUCT_DIR)/gradle/apk_sources.stamp' ],
+ 'action': [ 'touch', '<@(_outputs)' ],
+ }
+ ]
+ }
+ ]
+}
diff --git a/src/starboard/android/apk/app/CMakeLists.txt b/src/starboard/android/apk/app/CMakeLists.txt
new file mode 100644
index 0000000..3e0fd3c
--- /dev/null
+++ b/src/starboard/android/apk/app/CMakeLists.txt
@@ -0,0 +1,94 @@
+# Copyright 2016 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Since libcoat.so is an "IMPORTED" library in CMake, in order to debug in
+# Android Studio you have to manually set the debugger type so that LLDB will
+# be started:
+# Run -> Edit Configurations -> "app" -> Debugger -> Debug Type = Dual
+
+cmake_minimum_required(VERSION 3.4.1)
+
+# Map Android ABI to Cobalt architecture
+if(ANDROID_ABI STREQUAL x86)
+ set(COBALT_ARCH x86)
+elseif(ANDROID_ABI STREQUAL armeabi-v7a)
+ set(COBALT_ARCH arm)
+elseif(ANDROID_ABI STREQUAL arm64-v8a)
+ set(COBALT_ARCH arm64)
+else()
+ message(SEND_ERROR "Unsupported Android ABI: ${ANDROID_ABI}.")
+endif()
+
+# If COBALT_PRODUCT_DIR isn't set use the relative path up to the appropriate
+# 'out' directory.
+if(NOT COBALT_PRODUCT_DIR)
+ set(COBALT_PRODUCT_DIR
+ ${CMAKE_CURRENT_LIST_DIR}/../../../../out/android-${COBALT_ARCH}_${COBALT_CONFIG}
+ )
+endif()
+
+# If COBALT_CONTENT_DIR isn't set for a particular deploy target use the
+# 'content/data' directory.
+if(NOT COBALT_CONTENT_DIR)
+ set(COBALT_CONTENT_DIR
+ ${COBALT_PRODUCT_DIR}/content/data
+ )
+endif()
+
+# For platform deploy builds, use the -n parameter to skip Cobalt ninja and
+# just copy the .so that was already built and waiting in COBALT_PRODUCT_DIR.
+if(COBALT_PLATFORM_DEPLOY)
+ set(skip_ninja_arg -n)
+endif()
+
+# Run Cobalt ninja, and copy the result as our "IMPORTED" libcoat.so.
+# ("coat_lib" never gets created, so this runs every time.)
+add_custom_command(OUTPUT coat_lib
+ COMMAND ${CMAKE_CURRENT_LIST_DIR}/cobalt-ninja.sh
+ ${skip_ninja_arg} -C ${COBALT_PRODUCT_DIR} ${COBALT_TARGET}
+ COMMAND ${CMAKE_COMMAND} -E copy
+ ${COBALT_PRODUCT_DIR}/lib/lib${COBALT_TARGET}.so
+ ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/libcoat.so
+)
+
+# Make a symlink to the cobalt static content. We put it in a parent directory
+# of the library output, which corresponds to what the Gradle build sets for
+# android.sourceSets.<build-type>.assets.srcDir. This ends up overwriting the
+# symlink for each ABI built, but that's okay since the Cobalt static content
+# for a given build config is the same for all architectures.
+# ("cobalt_content" never gets created, so this runs every time.)
+add_custom_command(OUTPUT cobalt_content
+ DEPENDS coat_lib
+ COMMAND ${CMAKE_COMMAND} -E create_symlink
+ ${COBALT_CONTENT_DIR}
+ ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/../../cobalt_content
+)
+
+# We need a target (not a file) for the phony native dependency below.
+add_custom_target(external_cobalt_build DEPENDS coat_lib cobalt_content)
+
+# Declare libcoat.so as a shared library that needs to be included in the APK.
+# However, Android Studio will build it as an "IMPORTED" library.
+add_library(coat SHARED IMPORTED)
+set_target_properties(coat PROPERTIES
+ IMPORTED_LOCATION ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/libcoat.so
+)
+
+# Make a phony "native" library, so Android Studio has something to build.
+file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/phony.cpp "void __phony() {}")
+add_library(native SHARED ${CMAKE_CURRENT_BINARY_DIR}/phony.cpp)
+
+# Add a dependency to run the external cobalt build as a side effect of
+# building the phony native library.
+add_dependencies(native external_cobalt_build)
diff --git a/src/starboard/android/apk/app/build.gradle b/src/starboard/android/apk/app/build.gradle
new file mode 100644
index 0000000..ad09589
--- /dev/null
+++ b/src/starboard/android/apk/app/build.gradle
@@ -0,0 +1,162 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+apply plugin: 'com.android.application'
+
+final DEFAULT_COBALT_TARGET = 'cobalt'
+final String[] SUPPORTED_ABIS = [ 'x86', 'armeabi-v7a', 'arm64-v8a' ]
+
+ext {
+ // Provide defaults if these properties aren't specified on the command line.
+ buildAbi = project.hasProperty('cobaltBuildAbi') ? cobaltBuildAbi : SUPPORTED_ABIS
+ cobaltTarget = project.hasProperty('cobaltTarget') ? cobaltTarget : DEFAULT_COBALT_TARGET
+ cobaltProductDir =
+ project.hasProperty('cobaltProductDir') ? new File(cobaltProductDir).canonicalPath : ''
+ cobaltContentDir =
+ project.hasProperty('cobaltContentDir') ? new File(cobaltContentDir).canonicalPath : ''
+
+ buildIdFile = rootProject.file('build.id')
+ buildId = buildIdFile.exists() ? buildIdFile.text.trim() : '0'
+}
+
+println "TARGET: ${cobaltTarget}"
+
+android {
+ compileSdkVersion 28
+ buildToolsVersion "28.0.3"
+
+ signingConfigs {
+ // A signing config that matches what is implicitly used for the "debug" build type.
+ debugConfig {
+ keyAlias 'androiddebugkey'
+ keyPassword 'android'
+ storeFile file("${System.getProperty('user.home')}/.android/debug.keystore")
+ storePassword 'android'
+ }
+ }
+ defaultConfig {
+ applicationId "dev.cobalt.coat"
+ minSdkVersion 21
+ targetSdkVersion 28
+ versionCode 1
+ versionName "${buildId}"
+ manifestPlaceholders = [applicationName: "CoAT: ${cobaltTarget}"]
+ externalNativeBuild {
+ cmake {
+ arguments "-DCOBALT_TARGET=${cobaltTarget}"
+ arguments "-DCOBALT_PRODUCT_DIR=${cobaltProductDir}"
+ arguments "-DCOBALT_CONTENT_DIR=${cobaltContentDir}"
+ arguments "-DCOBALT_PLATFORM_DEPLOY=${project.hasProperty('cobaltDeployApk')}"
+ }
+ }
+ }
+ splits {
+ abi {
+ enable true
+ reset()
+ include buildAbi
+ }
+ }
+ buildTypes {
+ debug {
+ debuggable true
+ jniDebuggable true
+ externalNativeBuild {
+ cmake.arguments "-DCOBALT_CONFIG=debug"
+ }
+ signingConfig signingConfigs.debugConfig
+ }
+ devel {
+ debuggable true
+ jniDebuggable true
+ externalNativeBuild {
+ cmake.arguments "-DCOBALT_CONFIG=devel"
+ }
+ signingConfig signingConfigs.debugConfig
+ }
+ qa {
+ debuggable true
+ jniDebuggable true
+ externalNativeBuild {
+ cmake.arguments "-DCOBALT_CONFIG=qa"
+ }
+ signingConfig signingConfigs.debugConfig
+ }
+ release {
+ minifyEnabled true
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ externalNativeBuild {
+ cmake.arguments "-DCOBALT_CONFIG=gold"
+ }
+ signingConfig signingConfigs.debugConfig
+ }
+ }
+ sourceSets {
+ // The source is split into two parts:
+ // "main" is the code that should go into any Cobalt-based app for Android TV.
+ // "app" is the specialization needed to make "the blue app" out of the main code.
+ // The Android Gradle plugin includes "main" by default, so we just need to add "app".
+ main {
+ manifest.srcFile "src/app/AndroidManifest.xml"
+ java.srcDir "src/app/java"
+ res.srcDir "src/app/res"
+ assets.srcDir "src/app/assets"
+ }
+ // Add the directories symlinked by the CMake "cobalt_content" custom command.
+ debug {
+ assets.srcDir "${buildDir}/intermediates/cmake/debug/cobalt_content"
+ }
+ devel {
+ assets.srcDir "${buildDir}/intermediates/cmake/devel/cobalt_content"
+ }
+ qa {
+ assets.srcDir "${buildDir}/intermediates/cmake/qa/cobalt_content"
+ }
+ release {
+ assets.srcDir "${buildDir}/intermediates/cmake/release/cobalt_content"
+ }
+ }
+ externalNativeBuild {
+ cmake {
+ path 'CMakeLists.txt'
+ if (project.hasProperty('cobaltGradleDir')) {
+ // Resolve relative to the current dir at config time by getting a canonical File.
+ buildStagingDirectory new File(cobaltGradleDir, 'externalNativeBuild').canonicalFile
+ }
+ }
+ }
+}
+
+// When running in the platform deploy action, make tasks that depend on the appropriate Android
+// assemble tasks, then the move the assembled "app" apk to the desired path for deployment.
+if (project.hasProperty('cobaltDeployApk')) {
+ android.applicationVariants.all { variant ->
+ task "assembleCobalt_${variant.name}"(dependsOn: "assemble${variant.name.capitalize()}") {
+ def assembledApk = variant.outputs[0].outputFile
+ def deployApk = new File(cobaltDeployApk).canonicalFile
+ doLast {
+ assembledApk.renameTo(deployApk)
+ }
+ }
+ }
+}
+
+dependencies {
+ implementation fileTree(include: ['*.jar'], dir: 'libs')
+ implementation 'com.android.support:support-annotations:28.0.0'
+ implementation 'com.android.support:leanback-v17:28.0.0'
+ implementation 'com.android.support:support-v4:28.0.0'
+ implementation 'com.google.android.gms:play-services-auth:16.0.1'
+ implementation 'com.google.protobuf:protobuf-lite:3.0.1'
+}
diff --git a/src/starboard/android/apk/app/cobalt-ninja.sh b/src/starboard/android/apk/app/cobalt-ninja.sh
new file mode 100755
index 0000000..6dc1375
--- /dev/null
+++ b/src/starboard/android/apk/app/cobalt-ninja.sh
@@ -0,0 +1,47 @@
+#! /bin/bash
+# Copyright 2016 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# This wrapper allows us to run ninja without any assumptions about the
+# environment having been setup correctly to build Cobalt, since that won't
+# happen when started from Android Studio. See:
+# https://cobalt.googlesource.com/cobalt/+/master/src/#Building-and-Running-the-Code
+
+# Allow for a developer-specific environment setup from .cobaltrc
+# e.g., it may set DEPOT_TOOLS and/or setup some distributed build tools.
+local_rc=$(dirname $0)/.cobaltrc
+global_rc=${HOME}/.cobaltrc
+if [ -r ${local_rc} ]; then
+ source ${local_rc}
+elif [ -r ${global_rc} ]; then
+ source ${global_rc}
+fi
+
+# DEPOT_TOOLS may be set in .cobaltrc, otherwise assume it's in $HOME.
+[ -x ${DEPOT_TOOLS}/ninja ] || DEPOT_TOOLS=${HOME}/depot_tools
+
+# Use Cobalt's clang if it's not anywhere earlier in the PATH.
+SRC_DIR=$(cd $(dirname $0)/../../../..; pwd)
+PATH=$PATH:${SRC_DIR}/third_party/llvm-build/Release+Asserts/bin
+
+# Do nothing if -n is the first argument.
+if [ "$1" == "-n" ]; then
+ shift
+ echo "Skipping: ninja $@"
+ exit
+fi
+
+# When running in CMake, depot_tools isn't in the path, so we have to be
+# explicit about which ninja to run. Fail if we didn't find depot_tools.
+exec ${DEPOT_TOOLS}/ninja "$@"
diff --git a/src/starboard/android/apk/app/proguard-rules.pro b/src/starboard/android/apk/app/proguard-rules.pro
new file mode 100644
index 0000000..a4b8a54
--- /dev/null
+++ b/src/starboard/android/apk/app/proguard-rules.pro
@@ -0,0 +1,35 @@
+# Copyright 2016 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Proguard flags needed for the Android Starboard Java implementation to work
+# properly with its JNI native library.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# JNI is an entry point that's hard to keep track of, so the @UsedByNative
+# annotation marks fields and methods used by native code.
+
+# Annotations are implemented as attributes, so we have to explicitly keep them.
+-keepattributes *Annotation*
+
+# Keep classes, methods, and fields that are accessed with JNI.
+-keep @interface dev.cobalt.util.UsedByNative
+-keep @dev.cobalt.util.UsedByNative class *
+-keepclasseswithmembers class * {
+ @dev.cobalt.util.UsedByNative <methods>;
+}
+-keepclasseswithmembers class * {
+ @dev.cobalt.util.UsedByNative <fields>;
+}
diff --git a/src/starboard/android/apk/app/src/app/AndroidManifest.xml b/src/starboard/android/apk/app/src/app/AndroidManifest.xml
new file mode 100644
index 0000000..7337495
--- /dev/null
+++ b/src/starboard/android/apk/app/src/app/AndroidManifest.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2016 The Cobalt Authors. All Rights Reserved.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="dev.cobalt.coat">
+
+ <uses-feature android:name="android.hardware.microphone" android:required="false"/>
+ <uses-feature android:name="android.software.leanback" android:required="false"/>
+ <uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
+ <uses-feature android:glEsVersion="0x00020000" android:required="true"/>
+
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+ <uses-permission android:name="android.permission.GET_ACCOUNTS"/>
+ <uses-permission android:name="android.permission.USE_CREDENTIALS" android:maxSdkVersion="22"/>
+ <uses-permission android:name="android.permission.INTERNET"/>
+ <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
+ <uses-permission android:name="android.permission.RECORD_AUDIO"/>
+
+ <application
+ android:name="dev.cobalt.app.CobaltApplication"
+ android:icon="@mipmap/ic_app"
+ android:banner="@drawable/app_banner"
+ android:label="${applicationName}">
+
+ <activity android:name="dev.cobalt.app.MainActivity"
+ android:launchMode="singleTask"
+ android:configChanges="keyboard|keyboardHidden|navigation|orientation|screenSize|uiMode"
+ android:screenOrientation="sensorLandscape"
+ android:theme="@style/CobaltTheme">
+ <meta-data android:name="cobalt.APP_URL" android:value="https://www.youtube.com/tv"/>
+ <meta-data android:name="cobalt.SPLASH_URL" android:value="h5vcc-embedded://cobalt_splash_screen.html"/>
+ <meta-data android:name="android.app.lib_name" android:value="coat"/>
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ <category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW"/>
+ <action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ <category android:name="android.intent.category.BROWSABLE"/>
+ <data android:scheme="http"/>
+ <data android:scheme="https"/>
+ <data android:host="youtube.com"/>
+ <data android:host="www.youtube.com"/>
+ <data android:host="m.youtube.com"/>
+ <data android:host="youtu.be"/>
+ <data android:pathPattern=".*"/>
+ </intent-filter>
+ </activity>
+
+ </application>
+
+</manifest>
diff --git a/src/starboard/android/apk/app/src/app/assets/web/cobalt_blue_splash_screen.css b/src/starboard/android/apk/app/src/app/assets/web/cobalt_blue_splash_screen.css
new file mode 100644
index 0000000..e3ca851
--- /dev/null
+++ b/src/starboard/android/apk/app/src/app/assets/web/cobalt_blue_splash_screen.css
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2017 The Cobalt Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+
+body {
+ overflow: hidden;
+ font-size: 1.4815vh; /* Corresponds to 16px at 1080p. */
+}
+
+#splash {
+ background-color: #16396b;
+ background-image: url("file:///cobalt_logo_1024.png");
+ background-position: center center;
+ background-repeat: no-repeat;
+ background-size: auto 33%;
+ height: 100%;
+ left: 0;
+ position: absolute;
+ top: 0;
+ width: 100%;
+}
+
+#loading {
+ position: absolute;
+ top: 52em;
+ width: 100%;
+}
+
+#spinner {
+ /* The spinner starts with display set to none, and JavaScript will set this
+ to 'block' after some time has passed, if the splash screen is still
+ visible. */
+ display: none;
+
+ height: 5.33em;
+ margin: 0 auto;
+ position: relative;
+ width: 5.33em;
+}
+
+.dot {
+ background-color: #cbcbcb;
+ border-radius: 50%;
+ height: 1.17em;
+ position: absolute;
+ width: 1.17em;
+}
+
+@keyframes fade1 {
+ 0%,100% {opacity: 0}
+ 50% {opacity: 1}
+}
+
+@keyframes fade2 {
+ 0%,100% {opacity: .25}
+ 37.5% {opacity: 1}
+ 87.5% {opacity: 0}
+}
+
+@keyframes fade3 {
+ 0%,100% {opacity: .5}
+ 25% {opacity: 1}
+ 75% {opacity: 0}
+}
+
+@keyframes fade4 {
+ 0%,100% {opacity: .75}
+ 12.5% {opacity: 1}
+ 62.5% {opacity: 0}
+}
+
+@keyframes fade5 {
+ 0%,100% {opacity: 1}
+ 50% {opacity: 0}
+}
+
+@keyframes fade6 {
+ 0%,100% {opacity: .75}
+ 37.5% {opacity: 0}
+ 87.5% {opacity: 1}
+}
+
+@keyframes fade7 {
+ 0%,100% {opacity: .5}
+ 25% {opacity: 0}
+ 75% {opacity: 1}
+}
+
+@keyframes fade8 {
+ 0%,100% {opacity: .25}
+ 12.5% {opacity: 0}
+ 62.5% {opacity: 1}
+}
+
+#dot1 {
+ animation: fade8 .72s infinite ease;
+ left: 0;
+ top: 2.09em;
+}
+
+#dot2 {
+ animation: fade7 .72s infinite ease;
+ left: .61em;
+ top: .61em;
+}
+
+#dot3 {
+ animation: fade6 .72s infinite ease;
+ left: 2.09em;
+ top: 0;
+}
+
+#dot4 {
+ animation: fade5 .72s infinite ease;
+ right: .61em;
+ top: .61em;
+}
+
+#dot5 {
+ animation: fade4 .72s infinite ease;
+ right: 0;
+ top: 2.09em;
+}
+
+#dot6 {
+ animation: fade3 .72s infinite ease;
+ bottom: .61em;
+ right: .61em;
+}
+
+#dot7 {
+ animation: fade2 .72s infinite ease;
+ bottom: 0;
+ left: 2.09em;
+}
+
+#dot8 {
+ animation: fade1 .72s infinite ease;
+ bottom: .61em;
+ left: .61em;
+}
+
+.hidden {
+ height: 0;
+ visibility: hidden;
+}
diff --git a/src/starboard/android/apk/app/src/app/assets/web/cobalt_blue_splash_screen.html b/src/starboard/android/apk/app/src/app/assets/web/cobalt_blue_splash_screen.html
new file mode 100644
index 0000000..3a453a6
--- /dev/null
+++ b/src/starboard/android/apk/app/src/app/assets/web/cobalt_blue_splash_screen.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<!--
+ Copyright 2017 The Cobalt Authors. All Rights Reserved.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<html>
+
+<head>
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none';
+ script-src h5vcc-embedded://*/splash_screen.js;
+ style-src file://*/cobalt_blue_splash_screen.css;
+ img-src file://*/cobalt_logo_1024.png;">
+<link rel="stylesheet" type="text/css"
+ href="file:///cobalt_blue_splash_screen.css">
+</head>
+
+<body>
+ <div id="splash">
+ <img src="file:///cobalt_logo_1024.png" class="hidden">
+ </div>
+ <div id="loading">
+ <div id="spinner">
+ <div class="dot" id="dot1"></div>
+ <div class="dot" id="dot2"></div>
+ <div class="dot" id="dot3"></div>
+ <div class="dot" id="dot4"></div>
+ <div class="dot" id="dot5"></div>
+ <div class="dot" id="dot6"></div>
+ <div class="dot" id="dot7"></div>
+ <div class="dot" id="dot8"></div>
+ </div>
+ </div>
+ <script type="text/javascript" src="h5vcc-embedded://splash_screen.js">
+ </script>
+</body>
+
+</html>
diff --git a/src/starboard/android/apk/app/src/app/assets/web/cobalt_logo_1024.png b/src/starboard/android/apk/app/src/app/assets/web/cobalt_logo_1024.png
new file mode 100644
index 0000000..443a571
--- /dev/null
+++ b/src/starboard/android/apk/app/src/app/assets/web/cobalt_logo_1024.png
Binary files differ
diff --git a/src/starboard/android/apk/app/src/app/assets/web/link_android_splash_screen.html b/src/starboard/android/apk/app/src/app/assets/web/link_android_splash_screen.html
new file mode 100644
index 0000000..4aa1686
--- /dev/null
+++ b/src/starboard/android/apk/app/src/app/assets/web/link_android_splash_screen.html
@@ -0,0 +1,691 @@
+<!DOCTYPE html>
+<!--
+ Copyright 2017 The Cobalt Authors. All Rights Reserved.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<html>
+
+<head>
+ <meta http-equiv="Content-Security-Policy" content="
+ default-src 'unsafe-inline';
+ script-src file://*/cobalt_blue_splash_screen.html;
+ style-src 'unsafe-inline';"
+</head>
+
+<body style="background-color: #1f52a5;">
+ <link rel="splashscreen" href="file:///cobalt_blue_splash_screen.html">
+<h1>Heading</h1>
+
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
+interdum maximus finibus. Ut fermentum malesuada commodo. Sed
+faucibus, sapien a mattis lobortis, magna ante efficitur mauris, quis
+sodales nibh diam nec quam. Vestibulum magna libero, tincidunt non
+erat sed, molestie pulvinar ex. Maecenas semper blandit elit, id
+suscipit nulla venenatis pretium. Integer accumsan porta felis, vitae
+placerat urna accumsan vel. Aliquam eu aliquet mi. Aenean tincidunt
+eros lectus, sit amet efficitur orci ultrices at. Morbi lobortis ex
+quis luctus rutrum. In nulla velit, elementum vitae turpis vitae,
+finibus varius massa. Morbi id libero faucibus, tempus eros et,
+ullamcorper ipsum. Sed eleifend finibus bibendum. Nullam ut nunc nec
+lacus posuere dignissim. Nunc sollicitudin vitae augue id
+vulputate. Ut ac nibh gravida, volutpat est ac, facilisis neque.</p>
+
+<p>Nam dictum leo massa, non posuere dui bibendum id. Morbi sagittis est
+non est laoreet, a sollicitudin felis aliquet. Ut cursus vel leo a
+efficitur. Proin ut pellentesque sapien, vel maximus dui. Suspendisse
+eu felis eget leo elementum efficitur. Class aptent taciti sociosqu ad
+litora torquent per conubia nostra, per inceptos himenaeos. Fusce
+lobortis velit in elit pellentesque, ut auctor ipsum dignissim. Sed
+aliquet eleifend convallis. Duis mollis, dolor sed rutrum mollis,
+augue eros dignissim erat, eu dapibus augue turpis ac sapien. Morbi at
+volutpat odio, at molestie risus. Nulla quis nulla et magna vestibulum
+euismod. Praesent suscipit quam elit, non luctus turpis rutrum
+faucibus.</p>
+
+<p>Morbi feugiat lacus rhoncus, dignissim velit nec, dignissim
+lorem. Aliquam erat volutpat. Mauris semper dictum tempus. Nulla ex
+ligula, malesuada in ornare sed, euismod vitae massa. Etiam quis erat
+quis nisl facilisis suscipit. Mauris placerat ante et auctor
+fermentum. Donec tincidunt justo sem, ullamcorper vulputate nisl
+commodo a. Vestibulum quis ex non elit porttitor semper eget quis
+tortor. Suspendisse mattis neque non elementum scelerisque. Nulla
+facilisi. Nulla non felis et justo feugiat elementum. Aenean sodales
+turpis at erat eleifend lacinia. Proin eleifend volutpat purus id
+mollis. Proin vel tellus faucibus, sagittis libero at, lobortis
+odio. Praesent quam mauris, auctor vel velit eu, convallis molestie
+nisi. Pellentesque in nunc at orci ultrices vehicula.</p>
+
+<p>Praesent nibh lectus, efficitur sed risus in, rutrum tristique
+arcu. Curabitur non efficitur elit. Phasellus eget odio iaculis,
+molestie dui eget, venenatis erat. Nulla luctus facilisis lectus, nec
+dapibus tortor rhoncus vel. Donec nec arcu elit. Nullam ut faucibus
+purus, sed ultricies diam. Pellentesque at finibus ipsum. Vestibulum
+egestas dignissim nisl, ac rhoncus risus finibus sit amet. Donec non
+feugiat ante. Donec vehicula dui a lorem imperdiet, a tempus diam
+pulvinar. Nullam congue efficitur justo, non posuere ligula sodales
+in. Ut a urna ornare, ultrices velit in, pellentesque
+lorem. Vestibulum ante ipsum primis in faucibus orci luctus et
+ultrices posuere cubilia Curae;</p>
+
+<p>Orci varius natoque penatibus et magnis dis parturient montes,
+nascetur ridiculus mus. Morbi maximus quis magna et aliquet. Nam
+bibendum fermentum tempus. Praesent iaculis tortor metus, at
+vestibulum ipsum hendrerit mattis. Proin fringilla nisl sit amet
+tincidunt blandit. Interdum et malesuada fames ac ante ipsum primis in
+faucibus. Phasellus vel lectus leo. Curabitur fringilla, arcu non
+posuere viverra, urna metus blandit augue, convallis mattis tortor dui
+vel arcu. In sit amet metus vitae ex rhoncus hendrerit.</p>
+
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
+interdum maximus finibus. Ut fermentum malesuada commodo. Sed
+faucibus, sapien a mattis lobortis, magna ante efficitur mauris, quis
+sodales nibh diam nec quam. Vestibulum magna libero, tincidunt non
+erat sed, molestie pulvinar ex. Maecenas semper blandit elit, id
+suscipit nulla venenatis pretium. Integer accumsan porta felis, vitae
+placerat urna accumsan vel. Aliquam eu aliquet mi. Aenean tincidunt
+eros lectus, sit amet efficitur orci ultrices at. Morbi lobortis ex
+quis luctus rutrum. In nulla velit, elementum vitae turpis vitae,
+finibus varius massa. Morbi id libero faucibus, tempus eros et,
+ullamcorper ipsum. Sed eleifend finibus bibendum. Nullam ut nunc nec
+lacus posuere dignissim. Nunc sollicitudin vitae augue id
+vulputate. Ut ac nibh gravida, volutpat est ac, facilisis neque.</p>
+
+<p>Nam dictum leo massa, non posuere dui bibendum id. Morbi sagittis est
+non est laoreet, a sollicitudin felis aliquet. Ut cursus vel leo a
+efficitur. Proin ut pellentesque sapien, vel maximus dui. Suspendisse
+eu felis eget leo elementum efficitur. Class aptent taciti sociosqu ad
+litora torquent per conubia nostra, per inceptos himenaeos. Fusce
+lobortis velit in elit pellentesque, ut auctor ipsum dignissim. Sed
+aliquet eleifend convallis. Duis mollis, dolor sed rutrum mollis,
+augue eros dignissim erat, eu dapibus augue turpis ac sapien. Morbi at
+volutpat odio, at molestie risus. Nulla quis nulla et magna vestibulum
+euismod. Praesent suscipit quam elit, non luctus turpis rutrum
+faucibus.</p>
+
+<p>Morbi feugiat lacus rhoncus, dignissim velit nec, dignissim
+lorem. Aliquam erat volutpat. Mauris semper dictum tempus. Nulla ex
+ligula, malesuada in ornare sed, euismod vitae massa. Etiam quis erat
+quis nisl facilisis suscipit. Mauris placerat ante et auctor
+fermentum. Donec tincidunt justo sem, ullamcorper vulputate nisl
+commodo a. Vestibulum quis ex non elit porttitor semper eget quis
+tortor. Suspendisse mattis neque non elementum scelerisque. Nulla
+facilisi. Nulla non felis et justo feugiat elementum. Aenean sodales
+turpis at erat eleifend lacinia. Proin eleifend volutpat purus id
+mollis. Proin vel tellus faucibus, sagittis libero at, lobortis
+odio. Praesent quam mauris, auctor vel velit eu, convallis molestie
+nisi. Pellentesque in nunc at orci ultrices vehicula.</p>
+
+<p>Praesent nibh lectus, efficitur sed risus in, rutrum tristique
+arcu. Curabitur non efficitur elit. Phasellus eget odio iaculis,
+molestie dui eget, venenatis erat. Nulla luctus facilisis lectus, nec
+dapibus tortor rhoncus vel. Donec nec arcu elit. Nullam ut faucibus
+purus, sed ultricies diam. Pellentesque at finibus ipsum. Vestibulum
+egestas dignissim nisl, ac rhoncus risus finibus sit amet. Donec non
+feugiat ante. Donec vehicula dui a lorem imperdiet, a tempus diam
+pulvinar. Nullam congue efficitur justo, non posuere ligula sodales
+in. Ut a urna ornare, ultrices velit in, pellentesque
+lorem. Vestibulum ante ipsum primis in faucibus orci luctus et
+ultrices posuere cubilia Curae;</p>
+
+<p>Orci varius natoque penatibus et magnis dis parturient montes,
+nascetur ridiculus mus. Morbi maximus quis magna et aliquet. Nam
+bibendum fermentum tempus. Praesent iaculis tortor metus, at
+vestibulum ipsum hendrerit mattis. Proin fringilla nisl sit amet
+tincidunt blandit. Interdum et malesuada fames ac ante ipsum primis in
+faucibus. Phasellus vel lectus leo. Curabitur fringilla, arcu non
+posuere viverra, urna metus blandit augue, convallis mattis tortor dui
+vel arcu. In sit amet metus vitae ex rhoncus hendrerit.</p>
+
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
+interdum maximus finibus. Ut fermentum malesuada commodo. Sed
+faucibus, sapien a mattis lobortis, magna ante efficitur mauris, quis
+sodales nibh diam nec quam. Vestibulum magna libero, tincidunt non
+erat sed, molestie pulvinar ex. Maecenas semper blandit elit, id
+suscipit nulla venenatis pretium. Integer accumsan porta felis, vitae
+placerat urna accumsan vel. Aliquam eu aliquet mi. Aenean tincidunt
+eros lectus, sit amet efficitur orci ultrices at. Morbi lobortis ex
+quis luctus rutrum. In nulla velit, elementum vitae turpis vitae,
+finibus varius massa. Morbi id libero faucibus, tempus eros et,
+ullamcorper ipsum. Sed eleifend finibus bibendum. Nullam ut nunc nec
+lacus posuere dignissim. Nunc sollicitudin vitae augue id
+vulputate. Ut ac nibh gravida, volutpat est ac, facilisis neque.</p>
+
+<p>Nam dictum leo massa, non posuere dui bibendum id. Morbi sagittis est
+non est laoreet, a sollicitudin felis aliquet. Ut cursus vel leo a
+efficitur. Proin ut pellentesque sapien, vel maximus dui. Suspendisse
+eu felis eget leo elementum efficitur. Class aptent taciti sociosqu ad
+litora torquent per conubia nostra, per inceptos himenaeos. Fusce
+lobortis velit in elit pellentesque, ut auctor ipsum dignissim. Sed
+aliquet eleifend convallis. Duis mollis, dolor sed rutrum mollis,
+augue eros dignissim erat, eu dapibus augue turpis ac sapien. Morbi at
+volutpat odio, at molestie risus. Nulla quis nulla et magna vestibulum
+euismod. Praesent suscipit quam elit, non luctus turpis rutrum
+faucibus.</p>
+
+<p>Morbi feugiat lacus rhoncus, dignissim velit nec, dignissim
+lorem. Aliquam erat volutpat. Mauris semper dictum tempus. Nulla ex
+ligula, malesuada in ornare sed, euismod vitae massa. Etiam quis erat
+quis nisl facilisis suscipit. Mauris placerat ante et auctor
+fermentum. Donec tincidunt justo sem, ullamcorper vulputate nisl
+commodo a. Vestibulum quis ex non elit porttitor semper eget quis
+tortor. Suspendisse mattis neque non elementum scelerisque. Nulla
+facilisi. Nulla non felis et justo feugiat elementum. Aenean sodales
+turpis at erat eleifend lacinia. Proin eleifend volutpat purus id
+mollis. Proin vel tellus faucibus, sagittis libero at, lobortis
+odio. Praesent quam mauris, auctor vel velit eu, convallis molestie
+nisi. Pellentesque in nunc at orci ultrices vehicula.</p>
+
+<p>Praesent nibh lectus, efficitur sed risus in, rutrum tristique
+arcu. Curabitur non efficitur elit. Phasellus eget odio iaculis,
+molestie dui eget, venenatis erat. Nulla luctus facilisis lectus, nec
+dapibus tortor rhoncus vel. Donec nec arcu elit. Nullam ut faucibus
+purus, sed ultricies diam. Pellentesque at finibus ipsum. Vestibulum
+egestas dignissim nisl, ac rhoncus risus finibus sit amet. Donec non
+feugiat ante. Donec vehicula dui a lorem imperdiet, a tempus diam
+pulvinar. Nullam congue efficitur justo, non posuere ligula sodales
+in. Ut a urna ornare, ultrices velit in, pellentesque
+lorem. Vestibulum ante ipsum primis in faucibus orci luctus et
+ultrices posuere cubilia Curae;</p>
+
+<p>Orci varius natoque penatibus et magnis dis parturient montes,
+nascetur ridiculus mus. Morbi maximus quis magna et aliquet. Nam
+bibendum fermentum tempus. Praesent iaculis tortor metus, at
+vestibulum ipsum hendrerit mattis. Proin fringilla nisl sit amet
+tincidunt blandit. Interdum et malesuada fames ac ante ipsum primis in
+faucibus. Phasellus vel lectus leo. Curabitur fringilla, arcu non
+posuere viverra, urna metus blandit augue, convallis mattis tortor dui
+vel arcu. In sit amet metus vitae ex rhoncus hendrerit.</p>
+
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
+interdum maximus finibus. Ut fermentum malesuada commodo. Sed
+faucibus, sapien a mattis lobortis, magna ante efficitur mauris, quis
+sodales nibh diam nec quam. Vestibulum magna libero, tincidunt non
+erat sed, molestie pulvinar ex. Maecenas semper blandit elit, id
+suscipit nulla venenatis pretium. Integer accumsan porta felis, vitae
+placerat urna accumsan vel. Aliquam eu aliquet mi. Aenean tincidunt
+eros lectus, sit amet efficitur orci ultrices at. Morbi lobortis ex
+quis luctus rutrum. In nulla velit, elementum vitae turpis vitae,
+finibus varius massa. Morbi id libero faucibus, tempus eros et,
+ullamcorper ipsum. Sed eleifend finibus bibendum. Nullam ut nunc nec
+lacus posuere dignissim. Nunc sollicitudin vitae augue id
+vulputate. Ut ac nibh gravida, volutpat est ac, facilisis neque.</p>
+
+<p>Nam dictum leo massa, non posuere dui bibendum id. Morbi sagittis est
+non est laoreet, a sollicitudin felis aliquet. Ut cursus vel leo a
+efficitur. Proin ut pellentesque sapien, vel maximus dui. Suspendisse
+eu felis eget leo elementum efficitur. Class aptent taciti sociosqu ad
+litora torquent per conubia nostra, per inceptos himenaeos. Fusce
+lobortis velit in elit pellentesque, ut auctor ipsum dignissim. Sed
+aliquet eleifend convallis. Duis mollis, dolor sed rutrum mollis,
+augue eros dignissim erat, eu dapibus augue turpis ac sapien. Morbi at
+volutpat odio, at molestie risus. Nulla quis nulla et magna vestibulum
+euismod. Praesent suscipit quam elit, non luctus turpis rutrum
+faucibus.</p>
+
+<p>Morbi feugiat lacus rhoncus, dignissim velit nec, dignissim
+lorem. Aliquam erat volutpat. Mauris semper dictum tempus. Nulla ex
+ligula, malesuada in ornare sed, euismod vitae massa. Etiam quis erat
+quis nisl facilisis suscipit. Mauris placerat ante et auctor
+fermentum. Donec tincidunt justo sem, ullamcorper vulputate nisl
+commodo a. Vestibulum quis ex non elit porttitor semper eget quis
+tortor. Suspendisse mattis neque non elementum scelerisque. Nulla
+facilisi. Nulla non felis et justo feugiat elementum. Aenean sodales
+turpis at erat eleifend lacinia. Proin eleifend volutpat purus id
+mollis. Proin vel tellus faucibus, sagittis libero at, lobortis
+odio. Praesent quam mauris, auctor vel velit eu, convallis molestie
+nisi. Pellentesque in nunc at orci ultrices vehicula.</p>
+
+<p>Praesent nibh lectus, efficitur sed risus in, rutrum tristique
+arcu. Curabitur non efficitur elit. Phasellus eget odio iaculis,
+molestie dui eget, venenatis erat. Nulla luctus facilisis lectus, nec
+dapibus tortor rhoncus vel. Donec nec arcu elit. Nullam ut faucibus
+purus, sed ultricies diam. Pellentesque at finibus ipsum. Vestibulum
+egestas dignissim nisl, ac rhoncus risus finibus sit amet. Donec non
+feugiat ante. Donec vehicula dui a lorem imperdiet, a tempus diam
+pulvinar. Nullam congue efficitur justo, non posuere ligula sodales
+in. Ut a urna ornare, ultrices velit in, pellentesque
+lorem. Vestibulum ante ipsum primis in faucibus orci luctus et
+ultrices posuere cubilia Curae;</p>
+
+<p>Orci varius natoque penatibus et magnis dis parturient montes,
+nascetur ridiculus mus. Morbi maximus quis magna et aliquet. Nam
+bibendum fermentum tempus. Praesent iaculis tortor metus, at
+vestibulum ipsum hendrerit mattis. Proin fringilla nisl sit amet
+tincidunt blandit. Interdum et malesuada fames ac ante ipsum primis in
+faucibus. Phasellus vel lectus leo. Curabitur fringilla, arcu non
+posuere viverra, urna metus blandit augue, convallis mattis tortor dui
+vel arcu. In sit amet metus vitae ex rhoncus hendrerit.</p>
+
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
+interdum maximus finibus. Ut fermentum malesuada commodo. Sed
+faucibus, sapien a mattis lobortis, magna ante efficitur mauris, quis
+sodales nibh diam nec quam. Vestibulum magna libero, tincidunt non
+erat sed, molestie pulvinar ex. Maecenas semper blandit elit, id
+suscipit nulla venenatis pretium. Integer accumsan porta felis, vitae
+placerat urna accumsan vel. Aliquam eu aliquet mi. Aenean tincidunt
+eros lectus, sit amet efficitur orci ultrices at. Morbi lobortis ex
+quis luctus rutrum. In nulla velit, elementum vitae turpis vitae,
+finibus varius massa. Morbi id libero faucibus, tempus eros et,
+ullamcorper ipsum. Sed eleifend finibus bibendum. Nullam ut nunc nec
+lacus posuere dignissim. Nunc sollicitudin vitae augue id
+vulputate. Ut ac nibh gravida, volutpat est ac, facilisis neque.</p>
+
+<p>Nam dictum leo massa, non posuere dui bibendum id. Morbi sagittis est
+non est laoreet, a sollicitudin felis aliquet. Ut cursus vel leo a
+efficitur. Proin ut pellentesque sapien, vel maximus dui. Suspendisse
+eu felis eget leo elementum efficitur. Class aptent taciti sociosqu ad
+litora torquent per conubia nostra, per inceptos himenaeos. Fusce
+lobortis velit in elit pellentesque, ut auctor ipsum dignissim. Sed
+aliquet eleifend convallis. Duis mollis, dolor sed rutrum mollis,
+augue eros dignissim erat, eu dapibus augue turpis ac sapien. Morbi at
+volutpat odio, at molestie risus. Nulla quis nulla et magna vestibulum
+euismod. Praesent suscipit quam elit, non luctus turpis rutrum
+faucibus.</p>
+
+<p>Morbi feugiat lacus rhoncus, dignissim velit nec, dignissim
+lorem. Aliquam erat volutpat. Mauris semper dictum tempus. Nulla ex
+ligula, malesuada in ornare sed, euismod vitae massa. Etiam quis erat
+quis nisl facilisis suscipit. Mauris placerat ante et auctor
+fermentum. Donec tincidunt justo sem, ullamcorper vulputate nisl
+commodo a. Vestibulum quis ex non elit porttitor semper eget quis
+tortor. Suspendisse mattis neque non elementum scelerisque. Nulla
+facilisi. Nulla non felis et justo feugiat elementum. Aenean sodales
+turpis at erat eleifend lacinia. Proin eleifend volutpat purus id
+mollis. Proin vel tellus faucibus, sagittis libero at, lobortis
+odio. Praesent quam mauris, auctor vel velit eu, convallis molestie
+nisi. Pellentesque in nunc at orci ultrices vehicula.</p>
+
+<p>Praesent nibh lectus, efficitur sed risus in, rutrum tristique
+arcu. Curabitur non efficitur elit. Phasellus eget odio iaculis,
+molestie dui eget, venenatis erat. Nulla luctus facilisis lectus, nec
+dapibus tortor rhoncus vel. Donec nec arcu elit. Nullam ut faucibus
+purus, sed ultricies diam. Pellentesque at finibus ipsum. Vestibulum
+egestas dignissim nisl, ac rhoncus risus finibus sit amet. Donec non
+feugiat ante. Donec vehicula dui a lorem imperdiet, a tempus diam
+pulvinar. Nullam congue efficitur justo, non posuere ligula sodales
+in. Ut a urna ornare, ultrices velit in, pellentesque
+lorem. Vestibulum ante ipsum primis in faucibus orci luctus et
+ultrices posuere cubilia Curae;</p>
+
+<p>Orci varius natoque penatibus et magnis dis parturient montes,
+nascetur ridiculus mus. Morbi maximus quis magna et aliquet. Nam
+bibendum fermentum tempus. Praesent iaculis tortor metus, at
+vestibulum ipsum hendrerit mattis. Proin fringilla nisl sit amet
+tincidunt blandit. Interdum et malesuada fames ac ante ipsum primis in
+faucibus. Phasellus vel lectus leo. Curabitur fringilla, arcu non
+posuere viverra, urna metus blandit augue, convallis mattis tortor dui
+vel arcu. In sit amet metus vitae ex rhoncus hendrerit.</p>
+
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
+interdum maximus finibus. Ut fermentum malesuada commodo. Sed
+faucibus, sapien a mattis lobortis, magna ante efficitur mauris, quis
+sodales nibh diam nec quam. Vestibulum magna libero, tincidunt non
+erat sed, molestie pulvinar ex. Maecenas semper blandit elit, id
+suscipit nulla venenatis pretium. Integer accumsan porta felis, vitae
+placerat urna accumsan vel. Aliquam eu aliquet mi. Aenean tincidunt
+eros lectus, sit amet efficitur orci ultrices at. Morbi lobortis ex
+quis luctus rutrum. In nulla velit, elementum vitae turpis vitae,
+finibus varius massa. Morbi id libero faucibus, tempus eros et,
+ullamcorper ipsum. Sed eleifend finibus bibendum. Nullam ut nunc nec
+lacus posuere dignissim. Nunc sollicitudin vitae augue id
+vulputate. Ut ac nibh gravida, volutpat est ac, facilisis neque.</p>
+
+<p>Nam dictum leo massa, non posuere dui bibendum id. Morbi sagittis est
+non est laoreet, a sollicitudin felis aliquet. Ut cursus vel leo a
+efficitur. Proin ut pellentesque sapien, vel maximus dui. Suspendisse
+eu felis eget leo elementum efficitur. Class aptent taciti sociosqu ad
+litora torquent per conubia nostra, per inceptos himenaeos. Fusce
+lobortis velit in elit pellentesque, ut auctor ipsum dignissim. Sed
+aliquet eleifend convallis. Duis mollis, dolor sed rutrum mollis,
+augue eros dignissim erat, eu dapibus augue turpis ac sapien. Morbi at
+volutpat odio, at molestie risus. Nulla quis nulla et magna vestibulum
+euismod. Praesent suscipit quam elit, non luctus turpis rutrum
+faucibus.</p>
+
+<p>Morbi feugiat lacus rhoncus, dignissim velit nec, dignissim
+lorem. Aliquam erat volutpat. Mauris semper dictum tempus. Nulla ex
+ligula, malesuada in ornare sed, euismod vitae massa. Etiam quis erat
+quis nisl facilisis suscipit. Mauris placerat ante et auctor
+fermentum. Donec tincidunt justo sem, ullamcorper vulputate nisl
+commodo a. Vestibulum quis ex non elit porttitor semper eget quis
+tortor. Suspendisse mattis neque non elementum scelerisque. Nulla
+facilisi. Nulla non felis et justo feugiat elementum. Aenean sodales
+turpis at erat eleifend lacinia. Proin eleifend volutpat purus id
+mollis. Proin vel tellus faucibus, sagittis libero at, lobortis
+odio. Praesent quam mauris, auctor vel velit eu, convallis molestie
+nisi. Pellentesque in nunc at orci ultrices vehicula.</p>
+
+<p>Praesent nibh lectus, efficitur sed risus in, rutrum tristique
+arcu. Curabitur non efficitur elit. Phasellus eget odio iaculis,
+molestie dui eget, venenatis erat. Nulla luctus facilisis lectus, nec
+dapibus tortor rhoncus vel. Donec nec arcu elit. Nullam ut faucibus
+purus, sed ultricies diam. Pellentesque at finibus ipsum. Vestibulum
+egestas dignissim nisl, ac rhoncus risus finibus sit amet. Donec non
+feugiat ante. Donec vehicula dui a lorem imperdiet, a tempus diam
+pulvinar. Nullam congue efficitur justo, non posuere ligula sodales
+in. Ut a urna ornare, ultrices velit in, pellentesque
+lorem. Vestibulum ante ipsum primis in faucibus orci luctus et
+ultrices posuere cubilia Curae;</p>
+
+<p>Orci varius natoque penatibus et magnis dis parturient montes,
+nascetur ridiculus mus. Morbi maximus quis magna et aliquet. Nam
+bibendum fermentum tempus. Praesent iaculis tortor metus, at
+vestibulum ipsum hendrerit mattis. Proin fringilla nisl sit amet
+tincidunt blandit. Interdum et malesuada fames ac ante ipsum primis in
+faucibus. Phasellus vel lectus leo. Curabitur fringilla, arcu non
+posuere viverra, urna metus blandit augue, convallis mattis tortor dui
+vel arcu. In sit amet metus vitae ex rhoncus hendrerit.</p>
+
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
+interdum maximus finibus. Ut fermentum malesuada commodo. Sed
+faucibus, sapien a mattis lobortis, magna ante efficitur mauris, quis
+sodales nibh diam nec quam. Vestibulum magna libero, tincidunt non
+erat sed, molestie pulvinar ex. Maecenas semper blandit elit, id
+suscipit nulla venenatis pretium. Integer accumsan porta felis, vitae
+placerat urna accumsan vel. Aliquam eu aliquet mi. Aenean tincidunt
+eros lectus, sit amet efficitur orci ultrices at. Morbi lobortis ex
+quis luctus rutrum. In nulla velit, elementum vitae turpis vitae,
+finibus varius massa. Morbi id libero faucibus, tempus eros et,
+ullamcorper ipsum. Sed eleifend finibus bibendum. Nullam ut nunc nec
+lacus posuere dignissim. Nunc sollicitudin vitae augue id
+vulputate. Ut ac nibh gravida, volutpat est ac, facilisis neque.</p>
+
+<p>Nam dictum leo massa, non posuere dui bibendum id. Morbi sagittis est
+non est laoreet, a sollicitudin felis aliquet. Ut cursus vel leo a
+efficitur. Proin ut pellentesque sapien, vel maximus dui. Suspendisse
+eu felis eget leo elementum efficitur. Class aptent taciti sociosqu ad
+litora torquent per conubia nostra, per inceptos himenaeos. Fusce
+lobortis velit in elit pellentesque, ut auctor ipsum dignissim. Sed
+aliquet eleifend convallis. Duis mollis, dolor sed rutrum mollis,
+augue eros dignissim erat, eu dapibus augue turpis ac sapien. Morbi at
+volutpat odio, at molestie risus. Nulla quis nulla et magna vestibulum
+euismod. Praesent suscipit quam elit, non luctus turpis rutrum
+faucibus.</p>
+
+<p>Morbi feugiat lacus rhoncus, dignissim velit nec, dignissim
+lorem. Aliquam erat volutpat. Mauris semper dictum tempus. Nulla ex
+ligula, malesuada in ornare sed, euismod vitae massa. Etiam quis erat
+quis nisl facilisis suscipit. Mauris placerat ante et auctor
+fermentum. Donec tincidunt justo sem, ullamcorper vulputate nisl
+commodo a. Vestibulum quis ex non elit porttitor semper eget quis
+tortor. Suspendisse mattis neque non elementum scelerisque. Nulla
+facilisi. Nulla non felis et justo feugiat elementum. Aenean sodales
+turpis at erat eleifend lacinia. Proin eleifend volutpat purus id
+mollis. Proin vel tellus faucibus, sagittis libero at, lobortis
+odio. Praesent quam mauris, auctor vel velit eu, convallis molestie
+nisi. Pellentesque in nunc at orci ultrices vehicula.</p>
+
+<p>Praesent nibh lectus, efficitur sed risus in, rutrum tristique
+arcu. Curabitur non efficitur elit. Phasellus eget odio iaculis,
+molestie dui eget, venenatis erat. Nulla luctus facilisis lectus, nec
+dapibus tortor rhoncus vel. Donec nec arcu elit. Nullam ut faucibus
+purus, sed ultricies diam. Pellentesque at finibus ipsum. Vestibulum
+egestas dignissim nisl, ac rhoncus risus finibus sit amet. Donec non
+feugiat ante. Donec vehicula dui a lorem imperdiet, a tempus diam
+pulvinar. Nullam congue efficitur justo, non posuere ligula sodales
+in. Ut a urna ornare, ultrices velit in, pellentesque
+lorem. Vestibulum ante ipsum primis in faucibus orci luctus et
+ultrices posuere cubilia Curae;</p>
+
+<p>Orci varius natoque penatibus et magnis dis parturient montes,
+nascetur ridiculus mus. Morbi maximus quis magna et aliquet. Nam
+bibendum fermentum tempus. Praesent iaculis tortor metus, at
+vestibulum ipsum hendrerit mattis. Proin fringilla nisl sit amet
+tincidunt blandit. Interdum et malesuada fames ac ante ipsum primis in
+faucibus. Phasellus vel lectus leo. Curabitur fringilla, arcu non
+posuere viverra, urna metus blandit augue, convallis mattis tortor dui
+vel arcu. In sit amet metus vitae ex rhoncus hendrerit.</p>
+
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
+interdum maximus finibus. Ut fermentum malesuada commodo. Sed
+faucibus, sapien a mattis lobortis, magna ante efficitur mauris, quis
+sodales nibh diam nec quam. Vestibulum magna libero, tincidunt non
+erat sed, molestie pulvinar ex. Maecenas semper blandit elit, id
+suscipit nulla venenatis pretium. Integer accumsan porta felis, vitae
+placerat urna accumsan vel. Aliquam eu aliquet mi. Aenean tincidunt
+eros lectus, sit amet efficitur orci ultrices at. Morbi lobortis ex
+quis luctus rutrum. In nulla velit, elementum vitae turpis vitae,
+finibus varius massa. Morbi id libero faucibus, tempus eros et,
+ullamcorper ipsum. Sed eleifend finibus bibendum. Nullam ut nunc nec
+lacus posuere dignissim. Nunc sollicitudin vitae augue id
+vulputate. Ut ac nibh gravida, volutpat est ac, facilisis neque.</p>
+
+<p>Nam dictum leo massa, non posuere dui bibendum id. Morbi sagittis est
+non est laoreet, a sollicitudin felis aliquet. Ut cursus vel leo a
+efficitur. Proin ut pellentesque sapien, vel maximus dui. Suspendisse
+eu felis eget leo elementum efficitur. Class aptent taciti sociosqu ad
+litora torquent per conubia nostra, per inceptos himenaeos. Fusce
+lobortis velit in elit pellentesque, ut auctor ipsum dignissim. Sed
+aliquet eleifend convallis. Duis mollis, dolor sed rutrum mollis,
+augue eros dignissim erat, eu dapibus augue turpis ac sapien. Morbi at
+volutpat odio, at molestie risus. Nulla quis nulla et magna vestibulum
+euismod. Praesent suscipit quam elit, non luctus turpis rutrum
+faucibus.</p>
+
+<p>Morbi feugiat lacus rhoncus, dignissim velit nec, dignissim
+lorem. Aliquam erat volutpat. Mauris semper dictum tempus. Nulla ex
+ligula, malesuada in ornare sed, euismod vitae massa. Etiam quis erat
+quis nisl facilisis suscipit. Mauris placerat ante et auctor
+fermentum. Donec tincidunt justo sem, ullamcorper vulputate nisl
+commodo a. Vestibulum quis ex non elit porttitor semper eget quis
+tortor. Suspendisse mattis neque non elementum scelerisque. Nulla
+facilisi. Nulla non felis et justo feugiat elementum. Aenean sodales
+turpis at erat eleifend lacinia. Proin eleifend volutpat purus id
+mollis. Proin vel tellus faucibus, sagittis libero at, lobortis
+odio. Praesent quam mauris, auctor vel velit eu, convallis molestie
+nisi. Pellentesque in nunc at orci ultrices vehicula.</p>
+
+<p>Praesent nibh lectus, efficitur sed risus in, rutrum tristique
+arcu. Curabitur non efficitur elit. Phasellus eget odio iaculis,
+molestie dui eget, venenatis erat. Nulla luctus facilisis lectus, nec
+dapibus tortor rhoncus vel. Donec nec arcu elit. Nullam ut faucibus
+purus, sed ultricies diam. Pellentesque at finibus ipsum. Vestibulum
+egestas dignissim nisl, ac rhoncus risus finibus sit amet. Donec non
+feugiat ante. Donec vehicula dui a lorem imperdiet, a tempus diam
+pulvinar. Nullam congue efficitur justo, non posuere ligula sodales
+in. Ut a urna ornare, ultrices velit in, pellentesque
+lorem. Vestibulum ante ipsum primis in faucibus orci luctus et
+ultrices posuere cubilia Curae;</p>
+
+<p>Orci varius natoque penatibus et magnis dis parturient montes,
+nascetur ridiculus mus. Morbi maximus quis magna et aliquet. Nam
+bibendum fermentum tempus. Praesent iaculis tortor metus, at
+vestibulum ipsum hendrerit mattis. Proin fringilla nisl sit amet
+tincidunt blandit. Interdum et malesuada fames ac ante ipsum primis in
+faucibus. Phasellus vel lectus leo. Curabitur fringilla, arcu non
+posuere viverra, urna metus blandit augue, convallis mattis tortor dui
+vel arcu. In sit amet metus vitae ex rhoncus hendrerit.</p>
+
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
+interdum maximus finibus. Ut fermentum malesuada commodo. Sed
+faucibus, sapien a mattis lobortis, magna ante efficitur mauris, quis
+sodales nibh diam nec quam. Vestibulum magna libero, tincidunt non
+erat sed, molestie pulvinar ex. Maecenas semper blandit elit, id
+suscipit nulla venenatis pretium. Integer accumsan porta felis, vitae
+placerat urna accumsan vel. Aliquam eu aliquet mi. Aenean tincidunt
+eros lectus, sit amet efficitur orci ultrices at. Morbi lobortis ex
+quis luctus rutrum. In nulla velit, elementum vitae turpis vitae,
+finibus varius massa. Morbi id libero faucibus, tempus eros et,
+ullamcorper ipsum. Sed eleifend finibus bibendum. Nullam ut nunc nec
+lacus posuere dignissim. Nunc sollicitudin vitae augue id
+vulputate. Ut ac nibh gravida, volutpat est ac, facilisis neque.</p>
+
+<p>Nam dictum leo massa, non posuere dui bibendum id. Morbi sagittis est
+non est laoreet, a sollicitudin felis aliquet. Ut cursus vel leo a
+efficitur. Proin ut pellentesque sapien, vel maximus dui. Suspendisse
+eu felis eget leo elementum efficitur. Class aptent taciti sociosqu ad
+litora torquent per conubia nostra, per inceptos himenaeos. Fusce
+lobortis velit in elit pellentesque, ut auctor ipsum dignissim. Sed
+aliquet eleifend convallis. Duis mollis, dolor sed rutrum mollis,
+augue eros dignissim erat, eu dapibus augue turpis ac sapien. Morbi at
+volutpat odio, at molestie risus. Nulla quis nulla et magna vestibulum
+euismod. Praesent suscipit quam elit, non luctus turpis rutrum
+faucibus.</p>
+
+<p>Morbi feugiat lacus rhoncus, dignissim velit nec, dignissim
+lorem. Aliquam erat volutpat. Mauris semper dictum tempus. Nulla ex
+ligula, malesuada in ornare sed, euismod vitae massa. Etiam quis erat
+quis nisl facilisis suscipit. Mauris placerat ante et auctor
+fermentum. Donec tincidunt justo sem, ullamcorper vulputate nisl
+commodo a. Vestibulum quis ex non elit porttitor semper eget quis
+tortor. Suspendisse mattis neque non elementum scelerisque. Nulla
+facilisi. Nulla non felis et justo feugiat elementum. Aenean sodales
+turpis at erat eleifend lacinia. Proin eleifend volutpat purus id
+mollis. Proin vel tellus faucibus, sagittis libero at, lobortis
+odio. Praesent quam mauris, auctor vel velit eu, convallis molestie
+nisi. Pellentesque in nunc at orci ultrices vehicula.</p>
+
+<p>Praesent nibh lectus, efficitur sed risus in, rutrum tristique
+arcu. Curabitur non efficitur elit. Phasellus eget odio iaculis,
+molestie dui eget, venenatis erat. Nulla luctus facilisis lectus, nec
+dapibus tortor rhoncus vel. Donec nec arcu elit. Nullam ut faucibus
+purus, sed ultricies diam. Pellentesque at finibus ipsum. Vestibulum
+egestas dignissim nisl, ac rhoncus risus finibus sit amet. Donec non
+feugiat ante. Donec vehicula dui a lorem imperdiet, a tempus diam
+pulvinar. Nullam congue efficitur justo, non posuere ligula sodales
+in. Ut a urna ornare, ultrices velit in, pellentesque
+lorem. Vestibulum ante ipsum primis in faucibus orci luctus et
+ultrices posuere cubilia Curae;</p>
+
+<p>Orci varius natoque penatibus et magnis dis parturient montes,
+nascetur ridiculus mus. Morbi maximus quis magna et aliquet. Nam
+bibendum fermentum tempus. Praesent iaculis tortor metus, at
+vestibulum ipsum hendrerit mattis. Proin fringilla nisl sit amet
+tincidunt blandit. Interdum et malesuada fames ac ante ipsum primis in
+faucibus. Phasellus vel lectus leo. Curabitur fringilla, arcu non
+posuere viverra, urna metus blandit augue, convallis mattis tortor dui
+vel arcu. In sit amet metus vitae ex rhoncus hendrerit.</p>
+
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
+interdum maximus finibus. Ut fermentum malesuada commodo. Sed
+faucibus, sapien a mattis lobortis, magna ante efficitur mauris, quis
+sodales nibh diam nec quam. Vestibulum magna libero, tincidunt non
+erat sed, molestie pulvinar ex. Maecenas semper blandit elit, id
+suscipit nulla venenatis pretium. Integer accumsan porta felis, vitae
+placerat urna accumsan vel. Aliquam eu aliquet mi. Aenean tincidunt
+eros lectus, sit amet efficitur orci ultrices at. Morbi lobortis ex
+quis luctus rutrum. In nulla velit, elementum vitae turpis vitae,
+finibus varius massa. Morbi id libero faucibus, tempus eros et,
+ullamcorper ipsum. Sed eleifend finibus bibendum. Nullam ut nunc nec
+lacus posuere dignissim. Nunc sollicitudin vitae augue id
+vulputate. Ut ac nibh gravida, volutpat est ac, facilisis neque.</p>
+
+<p>Nam dictum leo massa, non posuere dui bibendum id. Morbi sagittis est
+non est laoreet, a sollicitudin felis aliquet. Ut cursus vel leo a
+efficitur. Proin ut pellentesque sapien, vel maximus dui. Suspendisse
+eu felis eget leo elementum efficitur. Class aptent taciti sociosqu ad
+litora torquent per conubia nostra, per inceptos himenaeos. Fusce
+lobortis velit in elit pellentesque, ut auctor ipsum dignissim. Sed
+aliquet eleifend convallis. Duis mollis, dolor sed rutrum mollis,
+augue eros dignissim erat, eu dapibus augue turpis ac sapien. Morbi at
+volutpat odio, at molestie risus. Nulla quis nulla et magna vestibulum
+euismod. Praesent suscipit quam elit, non luctus turpis rutrum
+faucibus.</p>
+
+<p>Morbi feugiat lacus rhoncus, dignissim velit nec, dignissim
+lorem. Aliquam erat volutpat. Mauris semper dictum tempus. Nulla ex
+ligula, malesuada in ornare sed, euismod vitae massa. Etiam quis erat
+quis nisl facilisis suscipit. Mauris placerat ante et auctor
+fermentum. Donec tincidunt justo sem, ullamcorper vulputate nisl
+commodo a. Vestibulum quis ex non elit porttitor semper eget quis
+tortor. Suspendisse mattis neque non elementum scelerisque. Nulla
+facilisi. Nulla non felis et justo feugiat elementum. Aenean sodales
+turpis at erat eleifend lacinia. Proin eleifend volutpat purus id
+mollis. Proin vel tellus faucibus, sagittis libero at, lobortis
+odio. Praesent quam mauris, auctor vel velit eu, convallis molestie
+nisi. Pellentesque in nunc at orci ultrices vehicula.</p>
+
+<p>Praesent nibh lectus, efficitur sed risus in, rutrum tristique
+arcu. Curabitur non efficitur elit. Phasellus eget odio iaculis,
+molestie dui eget, venenatis erat. Nulla luctus facilisis lectus, nec
+dapibus tortor rhoncus vel. Donec nec arcu elit. Nullam ut faucibus
+purus, sed ultricies diam. Pellentesque at finibus ipsum. Vestibulum
+egestas dignissim nisl, ac rhoncus risus finibus sit amet. Donec non
+feugiat ante. Donec vehicula dui a lorem imperdiet, a tempus diam
+pulvinar. Nullam congue efficitur justo, non posuere ligula sodales
+in. Ut a urna ornare, ultrices velit in, pellentesque
+lorem. Vestibulum ante ipsum primis in faucibus orci luctus et
+ultrices posuere cubilia Curae;</p>
+
+<p>Orci varius natoque penatibus et magnis dis parturient montes,
+nascetur ridiculus mus. Morbi maximus quis magna et aliquet. Nam
+bibendum fermentum tempus. Praesent iaculis tortor metus, at
+vestibulum ipsum hendrerit mattis. Proin fringilla nisl sit amet
+tincidunt blandit. Interdum et malesuada fames ac ante ipsum primis in
+faucibus. Phasellus vel lectus leo. Curabitur fringilla, arcu non
+posuere viverra, urna metus blandit augue, convallis mattis tortor dui
+vel arcu. In sit amet metus vitae ex rhoncus hendrerit.</p>
+
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
+interdum maximus finibus. Ut fermentum malesuada commodo. Sed
+faucibus, sapien a mattis lobortis, magna ante efficitur mauris, quis
+sodales nibh diam nec quam. Vestibulum magna libero, tincidunt non
+erat sed, molestie pulvinar ex. Maecenas semper blandit elit, id
+suscipit nulla venenatis pretium. Integer accumsan porta felis, vitae
+placerat urna accumsan vel. Aliquam eu aliquet mi. Aenean tincidunt
+eros lectus, sit amet efficitur orci ultrices at. Morbi lobortis ex
+quis luctus rutrum. In nulla velit, elementum vitae turpis vitae,
+finibus varius massa. Morbi id libero faucibus, tempus eros et,
+ullamcorper ipsum. Sed eleifend finibus bibendum. Nullam ut nunc nec
+lacus posuere dignissim. Nunc sollicitudin vitae augue id
+vulputate. Ut ac nibh gravida, volutpat est ac, facilisis neque.</p>
+
+<p>Nam dictum leo massa, non posuere dui bibendum id. Morbi sagittis est
+non est laoreet, a sollicitudin felis aliquet. Ut cursus vel leo a
+efficitur. Proin ut pellentesque sapien, vel maximus dui. Suspendisse
+eu felis eget leo elementum efficitur. Class aptent taciti sociosqu ad
+litora torquent per conubia nostra, per inceptos himenaeos. Fusce
+lobortis velit in elit pellentesque, ut auctor ipsum dignissim. Sed
+aliquet eleifend convallis. Duis mollis, dolor sed rutrum mollis,
+augue eros dignissim erat, eu dapibus augue turpis ac sapien. Morbi at
+volutpat odio, at molestie risus. Nulla quis nulla et magna vestibulum
+euismod. Praesent suscipit quam elit, non luctus turpis rutrum
+faucibus.</p>
+
+<p>Morbi feugiat lacus rhoncus, dignissim velit nec, dignissim
+lorem. Aliquam erat volutpat. Mauris semper dictum tempus. Nulla ex
+ligula, malesuada in ornare sed, euismod vitae massa. Etiam quis erat
+quis nisl facilisis suscipit. Mauris placerat ante et auctor
+fermentum. Donec tincidunt justo sem, ullamcorper vulputate nisl
+commodo a. Vestibulum quis ex non elit porttitor semper eget quis
+tortor. Suspendisse mattis neque non elementum scelerisque. Nulla
+facilisi. Nulla non felis et justo feugiat elementum. Aenean sodales
+turpis at erat eleifend lacinia. Proin eleifend volutpat purus id
+mollis. Proin vel tellus faucibus, sagittis libero at, lobortis
+odio. Praesent quam mauris, auctor vel velit eu, convallis molestie
+nisi. Pellentesque in nunc at orci ultrices vehicula.</p>
+
+<p>Praesent nibh lectus, efficitur sed risus in, rutrum tristique
+arcu. Curabitur non efficitur elit. Phasellus eget odio iaculis,
+molestie dui eget, venenatis erat. Nulla luctus facilisis lectus, nec
+dapibus tortor rhoncus vel. Donec nec arcu elit. Nullam ut faucibus
+purus, sed ultricies diam. Pellentesque at finibus ipsum. Vestibulum
+egestas dignissim nisl, ac rhoncus risus finibus sit amet. Donec non
+feugiat ante. Donec vehicula dui a lorem imperdiet, a tempus diam
+pulvinar. Nullam congue efficitur justo, non posuere ligula sodales
+in. Ut a urna ornare, ultrices velit in, pellentesque
+lorem. Vestibulum ante ipsum primis in faucibus orci luctus et
+ultrices posuere cubilia Curae;</p>
+
+<p>Orci varius natoque penatibus et magnis dis parturient montes,
+nascetur ridiculus mus. Morbi maximus quis magna et aliquet. Nam
+bibendum fermentum tempus. Praesent iaculis tortor metus, at
+vestibulum ipsum hendrerit mattis. Proin fringilla nisl sit amet
+tincidunt blandit. Interdum et malesuada fames ac ante ipsum primis in
+faucibus. Phasellus vel lectus leo. Curabitur fringilla, arcu non
+posuere viverra, urna metus blandit augue, convallis mattis tortor dui
+vel arcu. In sit amet metus vitae ex rhoncus hendrerit.</p>
+
+</body>
+</html>
diff --git a/src/starboard/android/apk/app/src/app/java/dev/cobalt/app/CobaltApplication.java b/src/starboard/android/apk/app/src/app/java/dev/cobalt/app/CobaltApplication.java
new file mode 100644
index 0000000..83f0a8e
--- /dev/null
+++ b/src/starboard/android/apk/app/src/app/java/dev/cobalt/app/CobaltApplication.java
@@ -0,0 +1,35 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.app;
+
+import android.app.Application;
+import dev.cobalt.coat.StarboardBridge;
+
+/**
+ * Android Application hosting the Starboard application.
+ */
+public class CobaltApplication extends Application implements StarboardBridge.HostApplication {
+ StarboardBridge starboardBridge;
+
+ @Override
+ public void setStarboardBridge(StarboardBridge starboardBridge) {
+ this.starboardBridge = starboardBridge;
+ }
+
+ @Override
+ public StarboardBridge getStarboardBridge() {
+ return starboardBridge;
+ }
+}
diff --git a/src/starboard/android/apk/app/src/app/java/dev/cobalt/app/MainActivity.java b/src/starboard/android/apk/app/src/app/java/dev/cobalt/app/MainActivity.java
new file mode 100644
index 0000000..04e70a8
--- /dev/null
+++ b/src/starboard/android/apk/app/src/app/java/dev/cobalt/app/MainActivity.java
@@ -0,0 +1,53 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.app;
+
+import android.app.Activity;
+import dev.cobalt.account.UserAuthorizerImpl;
+import dev.cobalt.coat.CobaltActivity;
+import dev.cobalt.coat.StarboardBridge;
+import dev.cobalt.feedback.NoopFeedbackService;
+import dev.cobalt.util.Holder;
+
+/**
+ * Main Activity for the "Cobalt on Android TV" app.
+ *
+ * <p>The real work is done in the abstract base class. This class is really just some factory
+ * methods to "inject" things that can be customized.
+ */
+public class MainActivity extends CobaltActivity {
+
+ @Override
+ protected StarboardBridge createStarboardBridge(String[] args, String startDeepLink) {
+ Holder<Activity> activityHolder = new Holder<>();
+ Runnable stopRequester =
+ new Runnable() {
+ @Override
+ public void run() {
+ getStarboardBridge().requestStop(0);
+ }
+ };
+ UserAuthorizerImpl userAuthorizer =
+ new UserAuthorizerImpl(getApplicationContext(), activityHolder, stopRequester);
+ NoopFeedbackService feedbackService = new NoopFeedbackService();
+ return new StarboardBridge(
+ getApplicationContext(),
+ activityHolder,
+ userAuthorizer,
+ feedbackService,
+ args,
+ startDeepLink);
+ }
+}
diff --git a/src/starboard/android/apk/app/src/app/res/drawable-xhdpi/app_banner.png b/src/starboard/android/apk/app/src/app/res/drawable-xhdpi/app_banner.png
new file mode 100644
index 0000000..fd47a4d
--- /dev/null
+++ b/src/starboard/android/apk/app/src/app/res/drawable-xhdpi/app_banner.png
Binary files differ
diff --git a/src/starboard/android/apk/app/src/app/res/mipmap-hdpi/ic_app.png b/src/starboard/android/apk/app/src/app/res/mipmap-hdpi/ic_app.png
new file mode 100644
index 0000000..44469e7
--- /dev/null
+++ b/src/starboard/android/apk/app/src/app/res/mipmap-hdpi/ic_app.png
Binary files differ
diff --git a/src/starboard/android/apk/app/src/app/res/mipmap-mdpi/ic_app.png b/src/starboard/android/apk/app/src/app/res/mipmap-mdpi/ic_app.png
new file mode 100644
index 0000000..8cd39ca
--- /dev/null
+++ b/src/starboard/android/apk/app/src/app/res/mipmap-mdpi/ic_app.png
Binary files differ
diff --git a/src/starboard/android/apk/app/src/app/res/mipmap-xhdpi/ic_app.png b/src/starboard/android/apk/app/src/app/res/mipmap-xhdpi/ic_app.png
new file mode 100644
index 0000000..4e12e56
--- /dev/null
+++ b/src/starboard/android/apk/app/src/app/res/mipmap-xhdpi/ic_app.png
Binary files differ
diff --git a/src/starboard/android/apk/app/src/app/res/mipmap-xxhdpi/ic_app.png b/src/starboard/android/apk/app/src/app/res/mipmap-xxhdpi/ic_app.png
new file mode 100644
index 0000000..3948b1e
--- /dev/null
+++ b/src/starboard/android/apk/app/src/app/res/mipmap-xxhdpi/ic_app.png
Binary files differ
diff --git a/src/starboard/android/apk/app/src/app/res/values/strings.xml b/src/starboard/android/apk/app/src/app/res/values/strings.xml
new file mode 100644
index 0000000..4b54b94
--- /dev/null
+++ b/src/starboard/android/apk/app/src/app/res/values/strings.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2017 The Cobalt Authors. All Rights Reserved.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <string name="app_name">Cobalt On Android TV</string>
+</resources>
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/account/AccessToken.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/account/AccessToken.java
new file mode 100644
index 0000000..9139b43
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/account/AccessToken.java
@@ -0,0 +1,49 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.account;
+
+import dev.cobalt.util.UsedByNative;
+
+/**
+ * POJO holding the token for a signed-in user.
+ */
+public class AccessToken {
+
+ private final String tokenValue;
+ private final long expirySeconds;
+
+ public AccessToken(String tokenValue, long expirySeconds) {
+ this.tokenValue = tokenValue;
+ this.expirySeconds = expirySeconds;
+ }
+
+ /**
+ * Returns the token value.
+ */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ public String getTokenValue() {
+ return tokenValue;
+ }
+
+ /**
+ * Returns number of seconds since epoch when this token expires, or 0 if it doesn't expire.
+ */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ public long getExpirySeconds() {
+ return expirySeconds;
+ }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/account/NoopUserAuthorizer.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/account/NoopUserAuthorizer.java
new file mode 100644
index 0000000..e995dd2
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/account/NoopUserAuthorizer.java
@@ -0,0 +1,59 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.account;
+
+import android.content.Intent;
+import dev.cobalt.util.UsedByNative;
+
+/** UserAuthorizer implementation that doesn't try to sign in. */
+public class NoopUserAuthorizer implements UserAuthorizer {
+
+ @Override
+ public void shutdown() {}
+
+ @Override
+ @SuppressWarnings("unused")
+ @UsedByNative
+ public void interrupt() {}
+
+ @Override
+ @SuppressWarnings("unused")
+ @UsedByNative
+ public AccessToken authorizeUser() {
+ return null;
+ }
+
+ @Override
+ @SuppressWarnings("unused")
+ @UsedByNative
+ public boolean deauthorizeUser() {
+ return false;
+ }
+
+ @Override
+ @SuppressWarnings("unused")
+ @UsedByNative
+ public AccessToken refreshAuthorization() {
+ return null;
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {}
+
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, String[] permissions, int[] grantResults) {}
+
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/account/UserAuthorizer.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/account/UserAuthorizer.java
new file mode 100644
index 0000000..1fc849e
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/account/UserAuthorizer.java
@@ -0,0 +1,86 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.account;
+
+import android.content.Intent;
+import dev.cobalt.util.UsedByNative;
+
+/**
+ * Java side implementation for starboard::android::shared::cobalt::AndroidUserAuthorizer.
+ */
+public interface UserAuthorizer {
+
+ /**
+ * Cleans up at the end of the object's lifecycle.
+ */
+ void shutdown();
+
+ /**
+ * Unblocks any pending request.
+ */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ void interrupt();
+
+ /**
+ * Prompts the user as necessary to choose an account, then gets an auth token for the selected
+ * account. I.e., pairs the web app to be signed-in as a selected account.
+ *
+ * This method blocks until finished, and must be called off the UI thread.
+ *
+ * Implementations must annotate this method with @UsedByNative so Proguard doesn't remove it.
+ */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ AccessToken authorizeUser();
+
+ /**
+ * Prompts the user as necessary to stop using an account. i.e., unpairs the web app to be
+ * signed-out.
+ *
+ * This method blocks until finished, and must be called off the UI thread.
+ *
+ * Implementations must annotate this method with @UsedByNative so Proguard doesn't remove it.
+ */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ boolean deauthorizeUser();
+
+ /**
+ * Gets a new auth token for the account most recently authorized.
+ *
+ * No UI will be shown if there is already a current account restored at construction or selected
+ * in authorizeUser(). If there is no current account, or the restored account has been deleted,
+ * the account picker may be shown to let the user specify which account should become the current
+ * account.
+ *
+ * This method blocks until finished, and must be called off the UI thread.
+ *
+ * Implementations must annotate this method with @UsedByNative so Proguard doesn't remove it.
+ */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ AccessToken refreshAuthorization();
+
+ /**
+ * @see android.app.Activity#onActivityResult(int, int, Intent)
+ */
+ void onActivityResult(int requestCode, int resultCode, Intent data);
+
+ /**
+ * @see android.app.Activity#onRequestPermissionsResult(int, String[], int[])
+ */
+ void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults);
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/account/UserAuthorizerImpl.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/account/UserAuthorizerImpl.java
new file mode 100644
index 0000000..136da8b
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/account/UserAuthorizerImpl.java
@@ -0,0 +1,532 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.account;
+
+import static android.Manifest.permission.GET_ACCOUNTS;
+import static dev.cobalt.util.Log.TAG;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.OnAccountsUpdateListener;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
+import android.text.TextUtils;
+import android.widget.Toast;
+import com.google.android.gms.auth.GoogleAuthException;
+import com.google.android.gms.auth.GoogleAuthUtil;
+import com.google.android.gms.auth.UserRecoverableAuthException;
+import com.google.android.gms.common.AccountPicker;
+import dev.cobalt.coat.R;
+import dev.cobalt.util.Holder;
+import dev.cobalt.util.Log;
+import dev.cobalt.util.UsedByNative;
+import java.io.IOException;
+
+/**
+ * Java side implementation for starboard::android::shared::cobalt::AndroidUserAuthorizer.
+ *
+ * This implements the following business logic:
+ * First run...
+ * - if there are no accounts, just be be signed-out
+ * - if there is one account, sign-in without any UI
+ * - if there are more than one accounts, prompt to choose account
+ * Subsequent runs...
+ * - sign-in to the same account last used to sign-in
+ * - if previously signed-out stay signed-out
+ * When user clicks 'sign-in' in the UI...
+ * - if there are no accounts, allow user to add an account
+ * - if there is one account, sign-in without any UI
+ * - if there are more than one accounts, prompt to choose account
+ * If the last signed-in account is deleted...
+ * - kill the app if stopped in the background to prompt next time it starts
+ * - at the next app start, show a toast that the account isn't available
+ * - if there are no accounts left, just be be signed-out
+ * - if there are one or more accounts left, prompt to choose account
+ */
+public class UserAuthorizerImpl implements OnAccountsUpdateListener, UserAuthorizer {
+
+ /** Pseudo account indicating the user chose to be signed-out. */
+ public static final Account SIGNED_OUT_ACCOUNT = new Account("-", "-");
+
+ /** Pseudo account indicating a saved account no longer exists. */
+ private static final Account MISSING_ACCOUNT = new Account("!", "!");
+
+ /** Foreshortened expiry of Google OAuth token, which typically lasts 1 hour. */
+ private static final long DEFAULT_EXPIRY_SECONDS = 5 * 60;
+
+ private static final String GOOGLE_ACCOUNT_TYPE = "com.google";
+ private static final String[] OAUTH_SCOPES = {
+ "https://www.googleapis.com/auth/youtube"
+ };
+
+ private static final String SHARED_PREFS_NAME = "user_auth";
+ private static final String ACCOUNT_NAME_PREF_KEY = "signed_in_account";
+
+ /** The thread on which the current request is running, or null if none. */
+ private volatile Thread requestThread;
+
+ private final Context appContext;
+ private final Holder<Activity> activityHolder;
+ private final Runnable stopRequester;
+ private final Handler mainHandler;
+
+ private Account currentAccount = null;
+ private AccessToken currentToken = null;
+
+ // Result from the account picker UI lands here.
+ private String chosenAccountName;
+
+ private volatile boolean waitingForPermission;
+ private volatile boolean permissionGranted;
+
+ public UserAuthorizerImpl(
+ Context appContext, Holder<Activity> activityHolder, Runnable stopRequester) {
+ this.appContext = appContext;
+ this.activityHolder = activityHolder;
+ this.stopRequester = stopRequester;
+ this.mainHandler = new Handler(Looper.getMainLooper());
+ addOnAccountsUpdatedListener(this);
+ }
+
+ @Override
+ public void shutdown() {
+ removeOnAccountsUpdatedListener(this);
+ }
+
+ @Override
+ @SuppressWarnings("unused")
+ @UsedByNative
+ public void interrupt() {
+ Thread t = requestThread;
+ if (t != null) {
+ t.interrupt();
+ }
+ }
+
+ @Override
+ @SuppressWarnings("unused")
+ @UsedByNative
+ public AccessToken authorizeUser() {
+ ensureBackgroundThread();
+ requestThread = Thread.currentThread();
+ // Let the user choose an account, or add one if there are none to choose.
+ // However, if there's only one account just choose it without any prompt.
+ currentAccount = autoSelectOrAddAccount();
+ writeAccountPref(currentAccount);
+ AccessToken accessToken = refreshCurrentToken();
+ requestThread = null;
+ return accessToken;
+ }
+
+ @Override
+ @SuppressWarnings("unused")
+ @UsedByNative
+ public boolean deauthorizeUser() {
+ ensureBackgroundThread();
+ requestThread = Thread.currentThread();
+ currentAccount = SIGNED_OUT_ACCOUNT;
+ writeAccountPref(currentAccount);
+ clearCurrentToken();
+ requestThread = null;
+ return true;
+ }
+
+ @Override
+ @SuppressWarnings("unused")
+ @UsedByNative
+ public AccessToken refreshAuthorization() {
+ ensureBackgroundThread();
+ requestThread = Thread.currentThread();
+
+ // If we haven't yet determined which account to use, check preferences for a saved account.
+ if (currentAccount == null) {
+ Account savedAccount = readAccountPref();
+ if (savedAccount == null) {
+ // No saved account, so this is the first ever run of the app.
+ currentAccount = autoSelectAccount();
+ } else if (savedAccount.equals(MISSING_ACCOUNT)) {
+ // The saved account got deleted.
+ currentAccount = forceSelectAccount();
+ } else {
+ // Use the saved account.
+ currentAccount = savedAccount;
+ }
+ writeAccountPref(currentAccount);
+ }
+
+ AccessToken accessToken = refreshCurrentToken();
+ requestThread = null;
+ return accessToken;
+ }
+
+ private static void ensureBackgroundThread() {
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ throw new UnsupportedOperationException("UserAuthorizer can't be called on main thread");
+ }
+ }
+
+ private void showToast(int resId, Object... formatArgs) {
+ final String msg = appContext.getResources().getString(resId, formatArgs);
+ mainHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(appContext, msg, Toast.LENGTH_LONG).show();
+ }
+ });
+ }
+
+ private Account readAccountPref() {
+ String savedAccountName = loadSignedInAccountName();
+ if (TextUtils.isEmpty(savedAccountName)) {
+ return null;
+ } else if (savedAccountName.equals(SIGNED_OUT_ACCOUNT.name)) {
+ // Don't request permissions or look for a device account if we were signed-out.
+ return SIGNED_OUT_ACCOUNT;
+ } else if (!checkPermission()) {
+ // We won't be able to get the account without permission, so warn the user and be signed-out.
+ showToast(R.string.starboard_missing_account, savedAccountName);
+ return SIGNED_OUT_ACCOUNT;
+ } else {
+ // Find the saved account name among all accounts on the device.
+ for (Account account : getAccounts()) {
+ if (account.name.equals(savedAccountName)) {
+ return account;
+ }
+ }
+ showToast(R.string.starboard_missing_account, savedAccountName);
+ return MISSING_ACCOUNT;
+ }
+ }
+
+ private void writeAccountPref(Account account) {
+ if (account == null) {
+ return;
+ }
+ // Always write the account name, even if it's the signed-out pseudo account.
+ saveSignedInAccountName(account.name);
+ }
+
+ private void clearCurrentToken() {
+ if (currentToken != null) {
+ clearToken(currentToken.getTokenValue());
+ currentToken = null;
+ }
+ }
+
+ private AccessToken refreshCurrentToken() {
+ clearCurrentToken();
+ if (currentAccount == null || SIGNED_OUT_ACCOUNT.equals(currentAccount)) {
+ return null;
+ }
+ String tokenValue = getToken(currentAccount);
+ if (tokenValue == null) {
+ showToast(R.string.starboard_account_auth_error);
+ tokenValue = "";
+ }
+ // TODO: Get the token details and use the actual expiry.
+ long expiry = System.currentTimeMillis() / 1000 + DEFAULT_EXPIRY_SECONDS;
+ currentToken = new AccessToken(tokenValue, expiry);
+ return currentToken;
+ }
+
+ /**
+ * Prompts the user to select an account, or to add an account if there are none. The prompt is
+ * skipped if there is exactly one account to choose from.
+ */
+ private Account autoSelectOrAddAccount() {
+ if (!checkPermission()) {
+ return SIGNED_OUT_ACCOUNT;
+ }
+ Account[] accounts = getAccounts();
+ if (accounts.length == 1) {
+ return accounts[0];
+ }
+ return selectOrAddAccount();
+ }
+
+ /**
+ * Prompts the user to select an account. The prompt is skipped if there are zero or one accounts
+ * to choose from.
+ */
+ private Account autoSelectAccount() {
+ if (!checkPermission()) {
+ return SIGNED_OUT_ACCOUNT;
+ }
+ Account[] accounts = getAccounts();
+ if (accounts.length == 0) {
+ return SIGNED_OUT_ACCOUNT;
+ } else if (accounts.length == 1) {
+ return accounts[0];
+ }
+ return selectOrAddAccount();
+ }
+
+ /**
+ * Prompts the user to select an account, even if there's only one to choose from. The prompt is
+ * skipped if there are zero accounts to choose from.
+ */
+ private Account forceSelectAccount() {
+ // We don't check permissions before calling selectOrAddAccount() because if the account is
+ // missing, readAccountPref() must have just checked, and we don't want to show permission
+ // prompt or rationale to the user twice.
+ Account[] accounts = getAccounts();
+ if (accounts.length == 0) {
+ return SIGNED_OUT_ACCOUNT;
+ }
+ return selectOrAddAccount();
+ }
+
+ /**
+ * Prompts the user to select an account, even if there's only one to choose from. If there are
+ * zero accounts to choose from, the user is prompted to add one.
+ *
+ * The caller should ensure permissions are granted before calling this method to avoid showing
+ * a picker with accounts that we can't access.
+ */
+ private Account selectOrAddAccount() {
+ String accountName = showAccountPicker();
+ // If user cancelled the picker stay signed-out.
+ if (TextUtils.isEmpty(accountName)) {
+ return SIGNED_OUT_ACCOUNT;
+ }
+ // Get the accounts after the picker in case one was added in the account picker.
+ for (Account account : getAccounts()) {
+ if (account.name.equals(accountName)) {
+ return account;
+ }
+ }
+ // This shouldn't happen, but if it does let the user know we're still signed-out.
+ Log.e(TAG, "Selected account is missing");
+ showToast(R.string.starboard_missing_account, accountName);
+ return SIGNED_OUT_ACCOUNT;
+ }
+
+ private synchronized String showAccountPicker() {
+ Activity activity = activityHolder.get();
+ Intent chooseAccountIntent = newChooseAccountIntent(currentAccount);
+ if (activity == null || chooseAccountIntent == null) {
+ return "";
+ }
+ chosenAccountName = null;
+ activity.startActivityForResult(chooseAccountIntent, R.id.rc_choose_account);
+
+ // Block until the account picker activity returns its result.
+ while (chosenAccountName == null) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ Log.e(TAG, "Account picker interrupted");
+ // Return empty string, as if the picker was cancelled.
+ return "";
+ }
+ }
+ return chosenAccountName;
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == R.id.rc_choose_account) {
+ String accountName = null;
+ if (resultCode == Activity.RESULT_OK) {
+ accountName = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME);
+ } else if (resultCode != Activity.RESULT_CANCELED) {
+ Log.e(TAG, "Account picker error " + resultCode);
+ showToast(R.string.starboard_account_picker_error);
+ }
+
+ // Notify showAccountPicker() which account was chosen.
+ synchronized (this) {
+ // Return empty string if the picker is cancelled or there's an unexpected result.
+ chosenAccountName = (accountName == null) ? "" : accountName;
+ notifyAll();
+ }
+ }
+ }
+
+ @Override
+ public void onAccountsUpdated(Account[] unused) {
+ if (currentAccount == null || SIGNED_OUT_ACCOUNT.equals(currentAccount)) {
+ // We're not signed-in; the update doesn't affect us.
+ return;
+ }
+ // Call getAccounts() since the param may not match the accounts we can access.
+ for (Account account : getAccounts()) {
+ if (account.name.equals(currentAccount.name)) {
+ // The current account is still there; the update doesn't affect us.
+ return;
+ }
+ }
+ // The current account is gone; leave the app so we prompt for sign-in next time.
+ // This should only happen while stopped in the background since we don't delete accounts.
+ stopRequester.run();
+ }
+
+ /**
+ * Calls framework AccountManager.addOnAccountsUpdatedListener().
+ */
+ private void addOnAccountsUpdatedListener(OnAccountsUpdateListener listener) {
+ AccountManager.get(appContext).addOnAccountsUpdatedListener(listener, null, false);
+ }
+
+ /**
+ * Calls framework AccountManager.removeOnAccountsUpdatedListener().
+ */
+ private void removeOnAccountsUpdatedListener(OnAccountsUpdateListener listener) {
+ AccountManager.get(appContext).removeOnAccountsUpdatedListener(listener);
+ }
+
+ /**
+ * Calls framework AccountManager.getAccountsByType() for Google accounts.
+ */
+ private Account[] getAccounts() {
+ return AccountManager.get(appContext).getAccountsByType(GOOGLE_ACCOUNT_TYPE);
+ }
+
+ /**
+ * Calls GMS AccountPicker.newChooseAccountIntent().
+ *
+ * Returns an Intent that when started will always show the account picker even if there's just
+ * one account on the device. If there are no accounts on the device it shows the UI to add one.
+ */
+ private Intent newChooseAccountIntent(Account defaultAccount) {
+ String[] allowableAccountTypes = {GOOGLE_ACCOUNT_TYPE};
+ return AccountPicker.newChooseAccountIntent(
+ defaultAccount, null, allowableAccountTypes, true, null, null, null, null);
+ }
+
+ /**
+ * Calls GMS GoogleAuthUtil.getToken(), without throwing any exceptions.
+ *
+ * Returns an empty string if no token is available for the account.
+ * Returns null if there was an error getting the token.
+ */
+ private String getToken(Account account) {
+ String joinedScopes = "oauth2:" + TextUtils.join(" ", OAUTH_SCOPES);
+ try {
+ return GoogleAuthUtil.getToken(appContext, account, joinedScopes);
+ } catch (UserRecoverableAuthException e) {
+ Log.w(TAG, "Recoverable error getting OAuth token", e);
+ Intent intent = e.getIntent();
+ Activity activity = activityHolder.get();
+ if (intent != null && activity != null) {
+ activity.startActivity(intent);
+ } else {
+ Log.e(TAG, "Failed to recover OAuth token", e);
+ }
+ return null;
+
+ } catch (IOException | GoogleAuthException e) {
+ Log.e(TAG, "Error getting auth token", e);
+ return null;
+ }
+ }
+
+ /**
+ * Calls GMS GoogleAuthUtil.clearToken(), without throwing any exceptions.
+ */
+ private void clearToken(String tokenValue) {
+ try {
+ GoogleAuthUtil.clearToken(appContext, tokenValue);
+ } catch (GoogleAuthException | IOException e) {
+ Log.e(TAG, "Error clearing auth token", e);
+ }
+ }
+
+ /**
+ * Checks whether the app has necessary permissions, asking for them if needed.
+ *
+ * This blocks until permissions are granted/declined, and should not be called on the UI thread.
+ *
+ * Returns true if permissions are granted.
+ */
+ private synchronized boolean checkPermission() {
+ if (ContextCompat.checkSelfPermission(appContext, GET_ACCOUNTS)
+ == PackageManager.PERMISSION_GRANTED) {
+ return true;
+ }
+
+ final Activity activity = activityHolder.get();
+ if (activity == null) {
+ return false;
+ }
+
+ // Check if we have previously been denied permission.
+ if (ActivityCompat.shouldShowRequestPermissionRationale(activity, GET_ACCOUNTS)) {
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(activity, R.string.starboard_accounts_permission, Toast.LENGTH_LONG)
+ .show();
+ }
+ });
+ return false;
+ }
+
+ // Request permission.
+ waitingForPermission = true;
+ permissionGranted = false;
+ ActivityCompat.requestPermissions(
+ activity, new String[]{GET_ACCOUNTS}, R.id.rc_get_accounts_permission);
+ try {
+ while (waitingForPermission) {
+ wait();
+ }
+ } catch (InterruptedException e) {
+ return false;
+ }
+ return permissionGranted;
+ }
+
+ /**
+ * Callback pass-thru from the Activity with the result from requesting permissions.
+ */
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, String[] permissions, int[] grantResults) {
+ if (requestCode == R.id.rc_get_accounts_permission) {
+ synchronized (this) {
+ permissionGranted = grantResults.length > 0
+ && grantResults[0] == PackageManager.PERMISSION_GRANTED;
+ waitingForPermission = false;
+ notifyAll();
+ }
+ }
+ }
+
+ /**
+ * Remember the name of the signed-in account.
+ */
+ private void saveSignedInAccountName(String accountName) {
+ getPreferences().edit().putString(ACCOUNT_NAME_PREF_KEY, accountName).commit();
+ }
+
+ /**
+ * Returns the remembered name of the signed-in account.
+ */
+ private String loadSignedInAccountName() {
+ return getPreferences().getString(ACCOUNT_NAME_PREF_KEY, "");
+ }
+
+ private SharedPreferences getPreferences() {
+ return appContext.getSharedPreferences(SHARED_PREFS_NAME, 0);
+ }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/AudioPermissionRequester.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/AudioPermissionRequester.java
new file mode 100644
index 0000000..f8640d8
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/AudioPermissionRequester.java
@@ -0,0 +1,82 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.coat;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
+import dev.cobalt.util.Holder;
+import dev.cobalt.util.UsedByNative;
+
+/** Helper class that requests the record audio permission. */
+public class AudioPermissionRequester {
+ private final Context context;
+ private final Holder<Activity> activityHolder;
+ private long nativePermissionRequestor;
+ // Only use in synchronized methods.
+ private boolean requestAudioPermissionStarted;
+
+ public AudioPermissionRequester(Context context, Holder<Activity> activityHolder) {
+ this.context = context;
+ this.activityHolder = activityHolder;
+ }
+
+ /**
+ * Requests the RECORD_AUDIO permission. Returns true if the permission is granted; returns false
+ * if the permission is not granted yet and starts to request the RECORD_AUDIO permission.
+ */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ public synchronized boolean requestRecordAudioPermission(long nativePermissionRequestor) {
+ this.nativePermissionRequestor = nativePermissionRequestor;
+ Activity activity = activityHolder.get();
+ if (activity == null) {
+ return false;
+ }
+
+ if (ContextCompat.checkSelfPermission(activity, Manifest.permission.RECORD_AUDIO)
+ == PackageManager.PERMISSION_GRANTED) {
+ return true;
+ }
+ if (!requestAudioPermissionStarted) {
+ ActivityCompat.requestPermissions(
+ activity, new String[] {Manifest.permission.RECORD_AUDIO}, R.id.rc_record_audio);
+ requestAudioPermissionStarted = true;
+ }
+
+ return false;
+ }
+
+ /** Handles the RECORD_AUDIO request result. */
+ public synchronized void onRequestPermissionsResult(
+ int requestCode, String[] permissions, int[] grantResults) {
+ if (requestCode == R.id.rc_record_audio) {
+ // If the request is cancelled, the result arrays are empty.
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ // Permission granted.
+ nativeHandlePermission(nativePermissionRequestor, true);
+ } else {
+ // Permission denied.
+ nativeHandlePermission(nativePermissionRequestor, false);
+ }
+ requestAudioPermissionStarted = false;
+ }
+ }
+
+ private native void nativeHandlePermission(long nativePermissionRequestor, boolean isGranted);
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltA11yHelper.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltA11yHelper.java
new file mode 100644
index 0000000..8c07518
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltA11yHelper.java
@@ -0,0 +1,261 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.coat;
+
+import static dev.cobalt.util.Log.TAG;
+
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v4.view.ViewCompat;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
+import android.support.v4.widget.ExploreByTouchHelper;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import dev.cobalt.util.Log;
+import java.util.BitSet;
+import java.util.List;
+
+/**
+ * An ExploreByTouchHelper that create a virtual d-pad grid, so that Cobalt remains functional when
+ * the TalkBack screen reader is enabled (which otherwise intercepts d-pad events for most
+ * applications).
+ */
+class CobaltA11yHelper extends ExploreByTouchHelper {
+ // These are from starboard/key.h
+ private static final int SB_KEY_GAMEPAD_DPAD_UP = 0x800C;
+ private static final int SB_KEY_GAMEPAD_DPAD_DOWN = 0x800D;
+ private static final int SB_KEY_GAMEPAD_DPAD_LEFT = 0x800E;
+ private static final int SB_KEY_GAMEPAD_DPAD_RIGHT = 0x800F;
+
+ // The fake dimensions for the nine virtual views.
+ // These values are arbitrary as long as the views stay on the screen.
+ private static final int FAKE_VIEW_HEIGHT = 10;
+ private static final int FAKE_VIEW_WIDTH = 10;
+
+ private int previousFocusedViewId = 1;
+ // This set tracks whether onPopulateNodeForVirtualView has been
+ // called for each virtual view id.
+ private final BitSet nodePopulatedSet = new BitSet(9);
+ private final Handler handler = new Handler();
+ private boolean hasInitialFocusBeenSet;
+
+ public CobaltA11yHelper(View view) {
+ super(view);
+ ViewCompat.setAccessibilityDelegate(view, this);
+ }
+
+ private static native void nativeInjectKeyEvent(int key);
+
+ @Override
+ protected int getVirtualViewAt(float x, float y) {
+ // This method is only required for touch or mouse interfaces.
+ // Since we don't support either, we simply always return HOST_ID.
+ return HOST_ID;
+ }
+
+ @Override
+ protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
+ if (!virtualViewIds.isEmpty()) {
+ throw new RuntimeException("Expected empty list");
+ }
+ // We always have precisely 9 virtual views.
+ for (int i = 1; i <= 9; i++) {
+ virtualViewIds.add(i);
+ }
+ }
+
+ /**
+ * Returns the "patch number" for a given view id, given a focused view id.
+ *
+ * <p>A "patch number" is a 1-9 number that describes where the requestedViewId is now located on
+ * an X-Y grid, given the focusedViewId.
+ *
+ * <p>Patch number grid:
+ * (0,0)----->X
+ * |+-+-+-+
+ * ||1|2|3|
+ * |+-+-+-|
+ * ||4|5|6|
+ * |+-+-+-|
+ * ||7|8|9|
+ * |+-+-+-+
+ * \./ Y
+ *
+ * <p>As focus changes, the locations of the views are moved so the focused view is always in the
+ * middle (patch number 5) and all of the other views always in the same relative position with
+ * respect to each other (with those on the edges adjacent to those on the opposite edges --
+ * wrapping around).
+ *
+ * <p>5 is returned whenever focusedViewId = requestedViewId
+ */
+ private static int getPatchNumber(int focusedViewId, int requestedViewId) {
+ // The (x,y) the focused view has in the 9 patch where 5 is in the middle.
+ int focusedX = (focusedViewId - 1) % 3;
+ int focusedY = (focusedViewId - 1) / 3;
+
+ // x and y offsets of focused view where middle is (0, 0)
+ int focusedRelativeToCenterX = focusedX - 1;
+ int focusedRelativeToCenterY = focusedY - 1;
+
+ // The (x,y) the requested view has in the 9 patch where 5 is in the middle.
+ int requestedX = (requestedViewId - 1) % 3;
+ int requestedY = (requestedViewId - 1) / 3;
+
+ // x and y offsets of requested view where middle is (0, 0)
+ int requestedRelativeToCenterX = requestedX - 1;
+ int requestedRelativeToCenterY = requestedY - 1;
+
+ // The (x,y) that the requested view has in the 9 patch when focusedViewId
+ // is in the middle.
+ int translatedRequestedX = (1 + 3 + requestedRelativeToCenterX - focusedRelativeToCenterX) % 3;
+ int translatedRequestedY = (1 + 3 + requestedRelativeToCenterY - focusedRelativeToCenterY) % 3;
+
+ return (translatedRequestedY * 3) + translatedRequestedX + 1;
+ }
+
+ private void maybeInjectEvent(int currentFocusedViewId) {
+ switch (getPatchNumber(previousFocusedViewId, currentFocusedViewId)) {
+ case 5:
+ // no move;
+ break;
+ case 2:
+ nativeInjectKeyEvent(SB_KEY_GAMEPAD_DPAD_UP);
+ break;
+ case 4:
+ nativeInjectKeyEvent(SB_KEY_GAMEPAD_DPAD_LEFT);
+ break;
+ case 6:
+ nativeInjectKeyEvent(SB_KEY_GAMEPAD_DPAD_RIGHT);
+ break;
+ case 8:
+ nativeInjectKeyEvent(SB_KEY_GAMEPAD_DPAD_DOWN);
+ break;
+ default:
+ // TODO: Could support diagonal movements, although it's likely
+ // not possible to reach this.
+ break;
+ }
+ previousFocusedViewId = currentFocusedViewId;
+ }
+
+ @Override
+ protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfoCompat node) {
+ int focusedViewId = getAccessibilityFocusedVirtualViewId();
+
+ if (focusedViewId < 1 || focusedViewId > 9) {
+ // If this is not one of our nine-patch views, it's probably HOST_ID
+ // In any case, assume there is no focus change.
+ focusedViewId = previousFocusedViewId;
+ }
+
+ // onPopulateNodeForVirtualView() gets called at least once every
+ // time the focused view changes. So see if it's changed since the
+ // last time we've been called and inject an event if so.
+ maybeInjectEvent(focusedViewId);
+
+ int patchNumber = getPatchNumber(focusedViewId, virtualViewId);
+
+ int x = (patchNumber - 1) % 3;
+ int y = (patchNumber - 1) / 3;
+
+ // Note that the specific bounds here are arbitrary. The importance
+ // is the relative bounds to each other.
+ node.setBoundsInParent(new Rect(
+ x * FAKE_VIEW_WIDTH,
+ y * FAKE_VIEW_HEIGHT,
+ x * FAKE_VIEW_WIDTH + FAKE_VIEW_WIDTH,
+ y * FAKE_VIEW_HEIGHT + FAKE_VIEW_HEIGHT));
+ node.setText("");
+
+ if (virtualViewId >= 1 || virtualViewId <= 9) {
+ nodePopulatedSet.set(virtualViewId - 1);
+ }
+ if (!hasInitialFocusBeenSet && nodePopulatedSet.cardinality() == 9) {
+ // Once the ExploreByTouchHelper knows about all of our virtual views,
+ // but not before, ask that the accessibility focus be moved from
+ // it's initial position on HOST_ID to the one we want to start with.
+ hasInitialFocusBeenSet = true;
+ handler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ sendEventForVirtualView(
+ previousFocusedViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
+ }
+ });
+ }
+ }
+
+ @Override
+ protected boolean onPerformActionForVirtualView(int virtualViewId, int action, Bundle arguments) {
+ return false;
+ }
+
+ /** A simple equivilent to Assert.assertEquals so we don't depend on junit */
+ private static void assertEquals(int expected, int actual) {
+ if (expected != actual) {
+ throw new RuntimeException("Expected " + expected + " actual " + actual);
+ }
+ }
+
+ /**
+ * Unit test for getPatchNumber().
+ *
+ * <p>As of this writing, the Java portion of the Cobalt build has no unit test mechanism.
+ *
+ * <p>To run this test, simply call it from application start and start the application.
+ *
+ * <p>TODO: Move this to a real unit test location when one exists.
+ */
+ private static void testGetPatchNumber() {
+ Log.i(TAG, "+testGetPatchNumber");
+
+ assertEquals(1, getPatchNumber(5, 1));
+ assertEquals(2, getPatchNumber(5, 2));
+ assertEquals(3, getPatchNumber(5, 3));
+ assertEquals(4, getPatchNumber(5, 4));
+ assertEquals(5, getPatchNumber(5, 5));
+ assertEquals(6, getPatchNumber(5, 6));
+ assertEquals(7, getPatchNumber(5, 7));
+ assertEquals(8, getPatchNumber(5, 8));
+ assertEquals(9, getPatchNumber(5, 9));
+
+ for (int i = 1; i <= 9; i++) {
+ assertEquals(5, getPatchNumber(i, i));
+ }
+
+ assertEquals(5, getPatchNumber(1, 1));
+ assertEquals(6, getPatchNumber(1, 2));
+ assertEquals(4, getPatchNumber(1, 3));
+ assertEquals(8, getPatchNumber(1, 4));
+ assertEquals(9, getPatchNumber(1, 5));
+ assertEquals(7, getPatchNumber(1, 6));
+ assertEquals(2, getPatchNumber(1, 7));
+ assertEquals(3, getPatchNumber(1, 8));
+ assertEquals(1, getPatchNumber(1, 9));
+
+ assertEquals(9, getPatchNumber(9, 1));
+ assertEquals(7, getPatchNumber(9, 2));
+ assertEquals(8, getPatchNumber(9, 3));
+ assertEquals(3, getPatchNumber(9, 4));
+ assertEquals(1, getPatchNumber(9, 5));
+ assertEquals(2, getPatchNumber(9, 6));
+ assertEquals(6, getPatchNumber(9, 7));
+ assertEquals(4, getPatchNumber(9, 8));
+ assertEquals(5, getPatchNumber(9, 9));
+ Log.i(TAG, "-testGetPatchNumber");
+ }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java
new file mode 100644
index 0000000..ecc84c8
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java
@@ -0,0 +1,266 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.coat;
+
+import static dev.cobalt.util.Log.TAG;
+
+import android.app.NativeActivity;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.ViewGroup.LayoutParams;
+import android.view.ViewTreeObserver;
+import android.widget.FrameLayout;
+import dev.cobalt.media.VideoSurfaceView;
+import dev.cobalt.util.Log;
+import dev.cobalt.util.UsedByNative;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/** Native activity that has the required JNI methods called by the Starboard implementation. */
+public abstract class CobaltActivity extends NativeActivity {
+
+ // A place to put args while debugging so they're used even when starting from the launcher.
+ // This should always be empty in submitted code.
+ private static final String[] DEBUG_ARGS = {};
+
+ private static final String URL_ARG = "--url=";
+ private static final java.lang.String META_DATA_APP_URL = "cobalt.APP_URL";
+
+ private static final String SPLASH_URL_ARG = "--fallback_splash_screen_url=";
+ private static final java.lang.String META_DATA_SPLASH_URL = "cobalt.SPLASH_URL";
+
+ private static final String FORCE_MIGRATION_FOR_STORAGE_PARTITIONING =
+ "--force_migration_for_storage_partitioning";
+ private static final String META_FORCE_MIGRATION_FOR_STORAGE_PARTITIONING =
+ "cobalt.force_migration_for_storage_partitioning";
+
+ @SuppressWarnings("unused")
+ private CobaltA11yHelper a11yHelper;
+
+ private VideoSurfaceView videoSurfaceView;
+ private KeyboardEditor keyboardEditor;
+
+ private ViewTreeObserver.OnGlobalLayoutListener videoSurfaceLayoutListener;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ // To ensure that volume controls adjust the correct stream, make this call
+ // early in the app's lifecycle. This connects the volume controls to
+ // STREAM_MUSIC whenever the target activity or fragment is visible.
+ setVolumeControlStream(AudioManager.STREAM_MUSIC);
+
+ String startDeepLink = getIntentUrlAsString(getIntent());
+ if (getStarboardBridge() == null) {
+ // Cold start - Instantiate the singleton StarboardBridge.
+ StarboardBridge starboardBridge = createStarboardBridge(getArgs(), startDeepLink);
+ ((StarboardBridge.HostApplication) getApplication()).setStarboardBridge(starboardBridge);
+ } else {
+ // Warm start - Pass the deep link to the running Starboard app.
+ getStarboardBridge().handleDeepLink(startDeepLink);
+ }
+
+ // super.onCreate() will cause an APP_CMD_START in native code,
+ // so make sure to initialize any state beforehand that might be touched by
+ // native code invocations.
+ super.onCreate(savedInstanceState);
+
+ videoSurfaceView = new VideoSurfaceView(this);
+ a11yHelper = new CobaltA11yHelper(videoSurfaceView);
+ addContentView(
+ videoSurfaceView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+
+ videoSurfaceLayoutListener =
+ new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ VideoSurfaceView.nativeOnGlobalLayout();
+ }
+ };
+ ViewTreeObserver observer = getWindow().getDecorView().getViewTreeObserver();
+ observer.addOnGlobalLayoutListener(videoSurfaceLayoutListener);
+
+ keyboardEditor = new KeyboardEditor(this);
+ addContentView(
+ keyboardEditor, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+ }
+
+ /**
+ * Instantiates the StarboardBridge. Apps not supporting sign-in should inject an instance of
+ * NoopUserAuthorizer. Apps may subclass StarboardBridge if they need to override anything.
+ */
+ protected abstract StarboardBridge createStarboardBridge(String[] args, String startDeepLink);
+
+ @UsedByNative
+ protected StarboardBridge getStarboardBridge() {
+ return ((StarboardBridge.HostApplication) getApplication()).getStarboardBridge();
+ }
+
+ @Override
+ protected void onStart() {
+ getStarboardBridge().onActivityStart(this, keyboardEditor);
+ super.onStart();
+ }
+
+ @Override
+ protected void onStop() {
+ getStarboardBridge().onActivityStop(this);
+ super.onStop();
+ }
+
+ @Override
+ protected void onDestroy() {
+ ViewTreeObserver observer = getWindow().getDecorView().getViewTreeObserver();
+ observer.removeOnGlobalLayoutListener(videoSurfaceLayoutListener);
+
+ super.onDestroy();
+ getStarboardBridge().onActivityDestroy(this);
+ }
+
+ @Override
+ public boolean onSearchRequested() {
+ return getStarboardBridge().onSearchRequested();
+ }
+
+ /** Returns true if the argument list contains an arg starting with argName. */
+ private static boolean hasArg(List<String> args, String argName) {
+ for (String arg : args) {
+ if (arg.startsWith(argName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Get argv/argc style args, if any from intent extras. Returns empty array if there are none
+ *
+ * <p>To use, invoke application via, eg, adb shell am start --esa args arg1,arg2 \
+ * dev.cobalt.coat/dev.cobalt.app.MainActivity
+ */
+ protected String[] getArgs() {
+ Bundle extras = getIntent().getExtras();
+ CharSequence[] argsExtra =
+ (extras == null || isReleaseBuild()) ? null : extras.getCharSequenceArray("args");
+
+ List<String> args = new ArrayList<>(Arrays.asList(DEBUG_ARGS));
+ if (argsExtra != null) {
+ for (int i = 0; i < argsExtra.length; i++) {
+ args.add(argsExtra[i].toString());
+ }
+ }
+
+ // If the URL arg isn't specified, get it from AndroidManifest.xml.
+ boolean hasUrlArg = hasArg(args, URL_ARG);
+ // If the splash screen url arg isn't specified, get it from AndroidManifest.xml.
+ boolean hasSplashUrlArg = hasArg(args, SPLASH_URL_ARG);
+ if (!hasUrlArg || !hasSplashUrlArg) {
+ try {
+ ActivityInfo ai =
+ getPackageManager()
+ .getActivityInfo(getIntent().getComponent(), PackageManager.GET_META_DATA);
+ if (ai.metaData != null) {
+ if (!hasUrlArg) {
+ String url = ai.metaData.getString(META_DATA_APP_URL);
+ if (url != null) {
+ args.add(URL_ARG + url);
+ }
+ }
+ if (!hasSplashUrlArg) {
+ String splashUrl = ai.metaData.getString(META_DATA_SPLASH_URL);
+ if (splashUrl != null) {
+ args.add(SPLASH_URL_ARG + splashUrl);
+ }
+ }
+ if (ai.metaData.getBoolean(META_FORCE_MIGRATION_FOR_STORAGE_PARTITIONING)) {
+ args.add(FORCE_MIGRATION_FOR_STORAGE_PARTITIONING);
+ }
+ }
+ } catch (NameNotFoundException e) {
+ throw new RuntimeException("Error getting activity info", e);
+ }
+ }
+
+ return args.toArray(new String[0]);
+ }
+
+ protected boolean isReleaseBuild() {
+ return StarboardBridge.isReleaseBuild();
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ getStarboardBridge().handleDeepLink(getIntentUrlAsString(intent));
+ }
+
+ /**
+ * Returns the URL from an Intent as a string. This may be overridden for additional processing.
+ */
+ protected String getIntentUrlAsString(Intent intent) {
+ Uri intentUri = intent.getData();
+ return (intentUri == null) ? null : intentUri.toString();
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ getStarboardBridge().onActivityResult(requestCode, resultCode, data);
+ }
+
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, String[] permissions, int[] grantResults) {
+ getStarboardBridge().onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
+
+ public void setVideoSurfaceBounds(final int x, final int y, final int width, final int height) {
+ if (!videoSurfaceView.updateVideoBounds(x, y, width, height)) {
+ return;
+ }
+
+ VideoSurfaceView.nativeOnLayoutNeeded();
+ runOnUiThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ VideoSurfaceView.nativeOnLayoutScheduled();
+ LayoutParams layoutParams = videoSurfaceView.getLayoutParams();
+ // Since videoSurfaceView is added directly to the Activity's content view, which is a
+ // FrameLayout, we expect its layout params to become FrameLayout.LayoutParams.
+ if (layoutParams instanceof FrameLayout.LayoutParams) {
+ ((FrameLayout.LayoutParams) layoutParams).setMargins(x, y, x + width, y + height);
+ } else {
+ Log.w(
+ TAG,
+ "Unexpected video surface layout params class "
+ + layoutParams.getClass().getName());
+ }
+ layoutParams.width = width;
+ layoutParams.height = height;
+ // Even though as a NativeActivity we're not using the Android UI framework, by setting
+ // the layout params it will force a layout to be requested. That will cause the
+ // SurfaceView to position its underlying Surface to match the screen coordinates of
+ // where the view would be in a UI layout and to set the surface transform matrix to
+ // match the view's size.
+ videoSurfaceView.setLayoutParams(layoutParams);
+ }
+ });
+ }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltHttpHelper.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltHttpHelper.java
new file mode 100644
index 0000000..56a2ffd
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltHttpHelper.java
@@ -0,0 +1,103 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.coat;
+
+import static dev.cobalt.util.Log.TAG;
+
+import dev.cobalt.util.Log;
+import java.io.BufferedInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+/** Helper class that implements an HTTP POST function used by DRM one-time provisioning. */
+public class CobaltHttpHelper {
+ private static final int RETRY_SLEEP_MILLIS = 250;
+ private static final int MAX_ATTEMPTS = 3;
+
+ /** Exception representing a transient HTTP failure (eg, 500). */
+ private static class TransientFailure extends Exception {}
+
+ private static void sleepBeforeRetry() {
+ try {
+ Thread.sleep(RETRY_SLEEP_MILLIS);
+ } catch (InterruptedException ex) {
+ // should never happen
+ }
+ }
+
+ /**
+ * Performs an HTTP POST, sending postData to url and returning the response contents on 200.
+ *
+ * <p>Note that this function retries temporary failures (network, HTTP 500) a few times.
+ *
+ * <p>Note also that this sets a few DRM-specific headers.
+ *
+ * @return response contents on success, null for permanent failure or exception.
+ */
+ public byte[] performDrmHttpPost(String url) {
+ for (int attempts = 0; attempts < MAX_ATTEMPTS; attempts++) {
+ try {
+ return internalPerformHttpPost(url);
+ } catch (IOException ex) {
+ Log.w(TAG, "performHttpPost IOException: ", ex);
+ // continue below
+ } catch (TransientFailure ex) {
+ // continue below
+ } catch (Throwable tr) {
+ // All other exceptions are caught because the caller is expected
+ // to be a JNI function, where exception handling is inconvenient.
+ Log.e(TAG, "performHttpPost exception: ", tr);
+ return null;
+ }
+ sleepBeforeRetry();
+ }
+ Log.w(TAG, "performHttpPost: Max attempts attempted");
+ return null;
+ }
+
+ private byte[] internalPerformHttpPost(String url) throws IOException, TransientFailure {
+ HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
+ try {
+ conn.setRequestMethod("POST");
+ conn.setDoOutput(false);
+ conn.setDoInput(true);
+
+ int statusCode = conn.getResponseCode();
+
+ if (statusCode >= 500 && statusCode <= 599) {
+ // We retry on 5xx failures.
+ Log.i(TAG, "performHttpPost transient failure: " + conn.getResponseMessage());
+ throw new TransientFailure();
+ }
+
+ if (statusCode != 200) {
+ Log.i(TAG, "performHttpPost permanent failure: " + conn.getResponseMessage());
+ return null;
+ }
+
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ BufferedInputStream input = new BufferedInputStream(conn.getInputStream());
+ for (int b = input.read(); b != -1; b = input.read()) {
+ output.write(b);
+ }
+
+ return output.toByteArray();
+ } finally {
+ conn.disconnect();
+ }
+ }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltTextToSpeechHelper.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltTextToSpeechHelper.java
new file mode 100644
index 0000000..f7809b0
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltTextToSpeechHelper.java
@@ -0,0 +1,206 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.coat;
+
+import static dev.cobalt.util.Log.TAG;
+
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.speech.tts.TextToSpeech;
+import android.view.accessibility.AccessibilityManager;
+import dev.cobalt.util.Log;
+import dev.cobalt.util.UsedByNative;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Helper class to implement the SbSpeechSynthesis* Starboard API for Audio accessibility.
+ *
+ * <p>This class is intended to be a singleton in the system. It creates a single static Handler
+ * thread in lieu of other synchronization options.
+ */
+class CobaltTextToSpeechHelper
+ implements TextToSpeech.OnInitListener,
+ AccessibilityManager.AccessibilityStateChangeListener,
+ AccessibilityManager.TouchExplorationStateChangeListener {
+ private final Context context;
+ private final Runnable stopRequester;
+ private final HandlerThread thread;
+ private final Handler handler;
+
+ // The TTS engine should be used only on the background thread.
+ private TextToSpeech ttsEngine;
+
+ private boolean wasScreenReaderEnabled;
+
+ private enum State {
+ PENDING,
+ INITIALIZED,
+ FAILED
+ }
+
+ // These are only accessed inside the Handler Thread
+ private State state = State.PENDING;
+ private long nextUtteranceId;
+ private final List<String> pendingUtterances = new ArrayList<>();
+
+ CobaltTextToSpeechHelper(Context context, Runnable stopRequester) {
+ this.context = context;
+ this.stopRequester = stopRequester;
+
+ thread = new HandlerThread("CobaltTextToSpeechHelper");
+ thread.start();
+ handler = new Handler(thread.getLooper());
+
+ AccessibilityManager accessibilityManager =
+ (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
+ wasScreenReaderEnabled = isScreenReaderEnabled();
+ accessibilityManager.addAccessibilityStateChangeListener(this);
+ accessibilityManager.addTouchExplorationStateChangeListener(this);
+ }
+
+ public void shutdown() {
+
+ handler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ if (ttsEngine != null) {
+ ttsEngine.shutdown();
+ }
+ }
+ });
+ thread.quitSafely();
+
+ AccessibilityManager accessibilityManager =
+ (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
+ accessibilityManager.removeAccessibilityStateChangeListener(this);
+ accessibilityManager.removeTouchExplorationStateChangeListener(this);
+ }
+
+ /** Returns whether a screen reader is currently enabled */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ public boolean isScreenReaderEnabled() {
+ AccessibilityManager am =
+ (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
+ final List<AccessibilityServiceInfo> screenReaders =
+ am.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_SPOKEN);
+ return !screenReaders.isEmpty();
+ }
+
+ /** Implementation of TextToSpeech.OnInitListener */
+ @Override
+ public void onInit(final int status) {
+ handler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ if (status != TextToSpeech.SUCCESS) {
+ Log.e(TAG, "TextToSpeech.onInit failure: " + status);
+ state = State.FAILED;
+ return;
+ }
+ state = State.INITIALIZED;
+ for (String utterance : pendingUtterances) {
+ speak(utterance);
+ }
+ pendingUtterances.clear();
+ }
+ });
+ }
+
+ /**
+ * Speaks the given text, enqueuing it if something is already speaking. Java-layer implementation
+ * of Starboard's SbSpeechSynthesisSpeak.
+ */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ void speak(final String text) {
+
+ handler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+
+ if (ttsEngine == null) {
+ ttsEngine = new TextToSpeech(context, CobaltTextToSpeechHelper.this);
+ }
+
+ switch (state) {
+ case PENDING:
+ pendingUtterances.add(text);
+ break;
+ case INITIALIZED:
+ int success =
+ ttsEngine.speak(
+ text, TextToSpeech.QUEUE_ADD, null, Long.toString(nextUtteranceId++));
+
+ if (success != TextToSpeech.SUCCESS) {
+ Log.e(TAG, "TextToSpeech.speak error: " + success);
+ return;
+ }
+ break;
+ case FAILED:
+ break;
+ }
+ }
+ });
+ }
+
+ /** Cancels all speaking. Java-layer implementation of Starboard's SbSpeechSynthesisCancel. */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ void cancel() {
+ handler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ if (ttsEngine != null) {
+ ttsEngine.stop();
+ }
+ pendingUtterances.clear();
+ }
+ });
+ }
+
+ @Override
+ public void onAccessibilityStateChanged(boolean enabled) {
+ // Note that this callback isn't perfect since it only tells us if accessibility was entirely
+ // enabled/disabled, but it's better than nothing. For example, it won't be called if the screen
+ // reader is enabled/disabled while text magnification remains enabled.
+ finishIfScreenReaderChanged();
+ }
+
+ @Override
+ public void onTouchExplorationStateChanged(boolean enabled) {
+ // We also listen for talkback changes because it's the standard (but not only) screen reader,
+ // and we can get a better signal than just listening for accessibility being enabled/disabled.
+ finishIfScreenReaderChanged();
+ }
+
+ /**
+ * Quit the app if screen reader settings changed so we respect the new setting the next time the
+ * app is run. This should only happen while stopped in the background since the user has to leave
+ * the app to change the setting.
+ */
+ private void finishIfScreenReaderChanged() {
+ if (wasScreenReaderEnabled != isScreenReaderEnabled()) {
+ stopRequester.run();
+ }
+ }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/ErrorDialog.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/ErrorDialog.java
new file mode 100644
index 0000000..35bc1f0
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/ErrorDialog.java
@@ -0,0 +1,125 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.coat;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+/**
+ * A fullscreen dialog to show an error, with up to 3 buttons. This has a look similar to the
+ * Android TV leanback ErrorFragment. As a dialog, it creates its own window so it can be shown on
+ * top of our NativeActivity, unlike a fragment.
+ */
+class ErrorDialog extends Dialog {
+
+ public static final int MAX_BUTTONS = 3;
+
+ private final Params params;
+
+ private static class Params {
+ private int messageId;
+ private int numButtons = 0;
+ private int[] buttonIds = new int[MAX_BUTTONS];
+ private int[] buttonLabelIds = new int[MAX_BUTTONS];
+ private OnClickListener buttonClickListener;
+ private OnDismissListener dismissListener;
+ }
+
+ public static class Builder {
+
+ private Context context;
+ private Params params = new Params();
+
+ public Builder(Context context) {
+ this.context = context;
+ }
+
+ public Builder setMessage(int messageId) {
+ params.messageId = messageId;
+ return this;
+ }
+
+ public Builder addButton(int id, int labelId) {
+ if (params.numButtons >= MAX_BUTTONS) {
+ throw new IllegalArgumentException("Too many buttons");
+ }
+ params.buttonIds[params.numButtons] = id;
+ params.buttonLabelIds[params.numButtons] = labelId;
+ params.numButtons++;
+ return this;
+ }
+
+ public Builder setButtonClickListener(OnClickListener buttonClickListener) {
+ params.buttonClickListener = buttonClickListener;
+ return this;
+ }
+
+ public Builder setOnDismissListener(OnDismissListener dismissListener) {
+ params.dismissListener = dismissListener;
+ return this;
+ }
+
+ public ErrorDialog create() {
+ return new ErrorDialog(context, params);
+ }
+ }
+
+ private ErrorDialog(Context context, Params params) {
+ super(context);
+ this.params = params;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.coat_error_dialog);
+
+ ImageView imageView = (ImageView) findViewById(R.id.image);
+ Drawable drawable = getContext().getResources().getDrawable(
+ R.drawable.lb_ic_sad_cloud, getContext().getTheme());
+ imageView.setImageDrawable(drawable);
+
+ TextView messageView = (TextView) findViewById(R.id.message);
+ messageView.setText(params.messageId);
+
+ Button button = (Button) findViewById(R.id.button_1);
+ ViewGroup container = (ViewGroup) button.getParent();
+ int buttonIndex = container.indexOfChild(button);
+
+ for (int i = 0; i < params.numButtons; i++) {
+ button = (Button) container.getChildAt(buttonIndex + i);
+ button.setText(params.buttonLabelIds[i]);
+ button.setVisibility(View.VISIBLE);
+
+ final int buttonId = params.buttonIds[i];
+ button.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ params.buttonClickListener.onClick(ErrorDialog.this, buttonId);
+ }
+ });
+ }
+
+ setOnDismissListener(params.dismissListener);
+ }
+
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/KeyboardEditor.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/KeyboardEditor.java
new file mode 100644
index 0000000..be5aa0f
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/KeyboardEditor.java
@@ -0,0 +1,107 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.coat;
+
+import android.app.Activity;
+import android.content.Context;
+import android.text.Editable;
+import android.text.Selection;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputMethodManager;
+
+/**
+ * The custom editor that receives text and displays it for the on-screen keyboard. It interacts
+ * with the Input Method Engine (IME) by receiving commands through the InputConnection interface
+ * and sending commands through InputMethodManager.
+ */
+public class KeyboardEditor extends View {
+ private final Context context;
+ private Editable editable;
+ private KeyboardInputConnection inputConnection;
+
+ public KeyboardEditor(Context context) {
+ this(context, null);
+ }
+
+ public KeyboardEditor(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ this.context = context;
+ setFocusable(true);
+ }
+
+ /**
+ * Create a new InputConnection for the on-screen keyboard InputMethod to interact with the view.
+ */
+ @Override
+ public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+ outAttrs.inputType = EditorInfo.TYPE_CLASS_TEXT;
+ outAttrs.inputType |= EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE;
+ outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN;
+ outAttrs.imeOptions |= EditorInfo.IME_ACTION_SEARCH;
+ outAttrs.initialSelStart = Selection.getSelectionStart(editable);
+ outAttrs.initialSelEnd = Selection.getSelectionEnd(editable);
+
+ this.inputConnection = new KeyboardInputConnection(context, this, outAttrs);
+ return inputConnection;
+ }
+
+ /** Show the on-screen keyboard. */
+ public void showKeyboard() {
+ final Activity activity = (Activity) context;
+ final KeyboardEditor view = this;
+
+ activity.runOnUiThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ view.setFocusable(true);
+ view.requestFocus();
+
+ InputMethodManager imm =
+ (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.showSoftInput(view, 0);
+ }
+ });
+ }
+
+ /** Hide the on-screen keyboard. */
+ public void hideKeyboard() {
+ final Activity activity = (Activity) context;
+ final KeyboardEditor view = this;
+
+ activity.runOnUiThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ view.setFocusable(true);
+ view.requestFocus();
+
+ InputMethodManager imm =
+ (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
+ }
+ });
+ }
+
+ /** Send the current state of the editable to the Input Method Manager. */
+ public void updateSelection(View view, int selStart, int selEnd, int compStart, int compEnd) {
+ InputMethodManager imm =
+ (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.updateSelection(view, selStart, selEnd, compStart, compEnd);
+ }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/KeyboardInputConnection.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/KeyboardInputConnection.java
new file mode 100644
index 0000000..99dc95a
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/KeyboardInputConnection.java
@@ -0,0 +1,174 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.coat;
+
+import android.content.Context;
+import android.text.Editable;
+import android.text.Selection;
+import android.text.TextUtils;
+import android.view.KeyEvent;
+import android.view.inputmethod.BaseInputConnection;
+import android.view.inputmethod.EditorInfo;
+
+/** The communication channel between the on-screen keyboard InputMethod and the KeyboardEditor. */
+public class KeyboardInputConnection extends BaseInputConnection {
+ private final KeyboardEditor keyboardEditor;
+ private final Context context;
+ private int numNestedBatchEdits = 0;
+
+ public KeyboardInputConnection(
+ Context context, KeyboardEditor keyboardEditor, EditorInfo outAttrs) {
+ super(keyboardEditor, true);
+ this.context = context;
+ this.keyboardEditor = keyboardEditor;
+ }
+
+ /**
+ * Start a batch edit, indicating to the editor that a batch of editor operations is occuring. The
+ * editor will avoid sending updates about its state until endBatchEdit() is called.
+ */
+ @Override
+ public boolean beginBatchEdit() {
+ numNestedBatchEdits++;
+ return super.beginBatchEdit();
+ }
+
+ /**
+ * End a batch edit, indicating to the editor that a batch edit previously initiated with
+ * beginBatchEdit() is done. This ends the latest batch only.
+ */
+ @Override
+ public boolean endBatchEdit() {
+ boolean result = super.endBatchEdit();
+ numNestedBatchEdits--;
+ updateEditingState();
+ return result;
+ }
+
+ /** Replace the currently composing text with the given text, and set the new cursor position. */
+ @Override
+ public boolean setComposingText(CharSequence text, int newCursorPosition) {
+ boolean result;
+ if (text.length() == 0) {
+ result = super.commitText(text, newCursorPosition);
+ } else {
+ result = super.setComposingText(text, newCursorPosition);
+ }
+
+ updateEditingState();
+ Editable editable = getEditable();
+ // TODO: Implement composition events for composing text.
+ nativeSendText(editable.toString());
+ return result;
+ }
+
+ /** Remove the composing state from the editable text. */
+ @Override
+ public boolean finishComposingText() {
+ boolean result = super.finishComposingText();
+ updateEditingState();
+ return result;
+ }
+
+ /** Change the selection position in the current editable text. */
+ @Override
+ public boolean setSelection(int start, int end) {
+ boolean result = super.setSelection(start, end);
+ updateEditingState();
+ return result;
+ }
+
+ /** Send the current state of the editable to the editor. */
+ private void updateEditingState() {
+ if (numNestedBatchEdits > 0) {
+ // The IME is in the middle of a batch edit; wait until it finishes.
+ return;
+ }
+
+ Editable editable = getEditable();
+ int selectionStart = Selection.getSelectionStart(editable);
+ int selectionEnd = Selection.getSelectionEnd(editable);
+ int composingStart = BaseInputConnection.getComposingSpanStart(editable);
+ int composingEnd = BaseInputConnection.getComposingSpanEnd(editable);
+ keyboardEditor.updateSelection(
+ keyboardEditor, selectionStart, selectionEnd, composingStart, composingEnd);
+ nativeSendText(editable.toString());
+ }
+
+ /** Send text to the search bar and set the new cursor position. */
+ @Override
+ public boolean commitText(CharSequence newText, int newCursorPosition) {
+ if (TextUtils.isEmpty(newText)) {
+ return false;
+ }
+ boolean result = super.commitText(newText, newCursorPosition);
+ updateEditingState();
+ return result;
+ }
+
+ /** Delete around the current selection position of the editable text. */
+ @Override
+ public boolean deleteSurroundingText(int leftLength, int rightLength) {
+ Editable editable = getEditable();
+ if (Selection.getSelectionStart(editable) == -1) {
+ return true;
+ }
+
+ boolean result = super.deleteSurroundingText(leftLength, rightLength);
+ updateEditingState();
+ return result;
+ }
+
+ /** Send a key event to the editor. */
+ @Override
+ public boolean sendKeyEvent(KeyEvent event) {
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
+ keyboardEditor.hideKeyboard();
+ } else if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
+ Editable editable = getEditable();
+ int selStart = Selection.getSelectionStart(editable);
+ int selEnd = Selection.getSelectionEnd(editable);
+ if (selEnd > selStart) {
+ // Delete the selection.
+ Selection.setSelection(editable, selStart);
+ editable.delete(selStart, selEnd);
+ updateEditingState();
+ return true;
+ } else if (selStart > 0) {
+ // Delete to the left of the cursor.
+ int newSel = Math.max(selStart - 1, 0);
+ Selection.setSelection(editable, newSel);
+ editable.delete(newSel, selStart);
+ updateEditingState();
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /** Have the editor perform an action associated with a specific key press. */
+ @Override
+ public boolean performEditorAction(int editorAction) {
+ if (editorAction == EditorInfo.IME_ACTION_SEARCH) {
+ // TODO: Implement keep focus where the keyboard is only hidden if there are search results.
+ keyboardEditor.hideKeyboard();
+ }
+ return true;
+ }
+
+ public static native void nativeSendText(CharSequence text);
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/PlatformError.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/PlatformError.java
new file mode 100644
index 0000000..4aa82e7
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/PlatformError.java
@@ -0,0 +1,153 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.coat;
+
+import static dev.cobalt.util.Log.TAG;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.Settings;
+import android.support.annotation.IntDef;
+import dev.cobalt.util.Holder;
+import dev.cobalt.util.Log;
+import dev.cobalt.util.UsedByNative;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Shows an ErrorDialog to inform the user of a Starboard platform error.
+ */
+public class PlatformError
+ implements DialogInterface.OnClickListener, DialogInterface.OnDismissListener {
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({CONNECTION_ERROR})
+ @interface ErrorType {}
+ // This must be kept in sync with starboard/android/shared/system_platform_error.cc
+ public static final int CONNECTION_ERROR = 0;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({CANCELLED, NEGATIVE, POSITIVE})
+ @interface Response {}
+ public static final int NEGATIVE = -1;
+ public static final int CANCELLED = 0;
+ public static final int POSITIVE = 1;
+
+ // Button IDs for CONNECTION_ERROR
+ private static final int RETRY_BUTTON = 1;
+ private static final int NETWORK_SETTINGS_BUTTON = 2;
+
+ private final Holder<Activity> activityHolder;
+ private final @ErrorType int errorType;
+ private final long data;
+ private final Handler uiThreadHandler;
+
+ private Dialog dialog;
+ private int response;
+
+ public PlatformError(Holder<Activity> activityHolder, @ErrorType int errorType, long data) {
+ this.activityHolder = activityHolder;
+ this.errorType = errorType;
+ this.data = data;
+ uiThreadHandler = new Handler(Looper.getMainLooper());
+ response = CANCELLED;
+ }
+
+ /** Display the error. */
+ public void raise() {
+ uiThreadHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ showDialogOnUiThread();
+ }
+ });
+ }
+
+ private void showDialogOnUiThread() {
+ Activity activity = activityHolder.get();
+ if (activity == null) {
+ onCleared(CANCELLED, data);
+ return;
+ }
+ ErrorDialog.Builder dialogBuilder = new ErrorDialog.Builder(activity);
+ switch (errorType) {
+ case CONNECTION_ERROR:
+ dialogBuilder
+ .setMessage(R.string.starboard_platform_connection_error)
+ .addButton(RETRY_BUTTON, R.string.starboard_platform_retry)
+ .addButton(NETWORK_SETTINGS_BUTTON, R.string.starboard_platform_network_settings);
+ break;
+ default:
+ Log.e(TAG, "Unknown platform error " + errorType);
+ return;
+ }
+ dialog = dialogBuilder
+ .setButtonClickListener(this)
+ .setOnDismissListener(this)
+ .create();
+ dialog.show();
+ }
+
+ /** Programmatically dismiss the error. */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ public void clear() {
+ uiThreadHandler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ if (dialog != null) {
+ dialog.dismiss();
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onClick(DialogInterface dialogInterface, int whichButton) {
+ if (errorType == CONNECTION_ERROR) {
+ switch (whichButton) {
+ case NETWORK_SETTINGS_BUTTON:
+ Activity activity = activityHolder.get();
+ if (activity != null) {
+ activity.startActivity(new Intent(Settings.ACTION_WIFI_SETTINGS));
+ }
+ break;
+ case RETRY_BUTTON:
+ response = POSITIVE;
+ dialog.dismiss();
+ break;
+ default: // fall out
+ }
+ }
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialogInterface) {
+ dialog = null;
+ onCleared(response, data);
+ }
+
+ /** Informs Starboard when the error is dismissed. */
+ protected void onCleared(@PlatformError.Response int response, long data) {
+ nativeOnCleared(response, data);
+ }
+
+ private native void nativeOnCleared(@PlatformError.Response int response, long data);
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java
new file mode 100644
index 0000000..6e7cb4b
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java
@@ -0,0 +1,557 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.coat;
+
+import static android.content.Context.AUDIO_SERVICE;
+import static android.media.AudioManager.GET_DEVICES_INPUTS;
+import static dev.cobalt.util.Log.TAG;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Build;
+import android.util.Size;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.CaptioningManager;
+import dev.cobalt.account.UserAuthorizer;
+import dev.cobalt.feedback.FeedbackService;
+import dev.cobalt.media.AudioOutputManager;
+import dev.cobalt.media.CaptionSettings;
+import dev.cobalt.media.CobaltMediaSession;
+import dev.cobalt.media.MediaImage;
+import dev.cobalt.util.DisplayUtil;
+import dev.cobalt.util.Holder;
+import dev.cobalt.util.Log;
+import dev.cobalt.util.UsedByNative;
+import java.lang.reflect.Method;
+import java.net.InterfaceAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Locale;
+
+/** Implementation of the required JNI methods called by the Starboard C++ code. */
+public class StarboardBridge {
+
+ /** Interface to be implemented by the Android Application hosting the starboard app. */
+ public interface HostApplication {
+ void setStarboardBridge(StarboardBridge starboardBridge);
+
+ StarboardBridge getStarboardBridge();
+ }
+
+ private CobaltTextToSpeechHelper ttsHelper;
+ private UserAuthorizer userAuthorizer;
+ private FeedbackService feedbackService;
+ private AudioOutputManager audioOutputManager;
+ private CobaltMediaSession cobaltMediaSession;
+ private VoiceRecognizer voiceRecognizer;
+ private AudioPermissionRequester audioPermissionRequester;
+ private KeyboardEditor keyboardEditor;
+
+ static {
+ // Even though NativeActivity already loads our library from C++,
+ // we still have to load it from Java to make JNI calls into it.
+ System.loadLibrary("coat");
+ }
+
+ private final Context appContext;
+ private final Holder<Activity> activityHolder;
+ private final String[] args;
+ private final String startDeepLink;
+ private final Runnable stopRequester =
+ new Runnable() {
+ @Override
+ public void run() {
+ requestStop(0);
+ }
+ };
+
+ private volatile boolean starboardStopped = false;
+
+ public StarboardBridge(
+ Context appContext,
+ Holder<Activity> activityHolder,
+ UserAuthorizer userAuthorizer,
+ FeedbackService feedbackService,
+ String[] args,
+ String startDeepLink) {
+
+ // Make sure the JNI stack is properly initialized first as there is
+ // race condition as soon as any of the following objects creates a new thread.
+ nativeInitialize();
+
+ this.appContext = appContext;
+ this.activityHolder = activityHolder;
+ this.args = args;
+ this.startDeepLink = startDeepLink;
+ this.ttsHelper = new CobaltTextToSpeechHelper(appContext, stopRequester);
+ this.userAuthorizer = userAuthorizer;
+ this.feedbackService = feedbackService;
+ this.audioOutputManager = new AudioOutputManager(appContext);
+ this.cobaltMediaSession =
+ new CobaltMediaSession(appContext, activityHolder, audioOutputManager);
+ this.audioPermissionRequester = new AudioPermissionRequester(appContext, activityHolder);
+ this.voiceRecognizer =
+ new VoiceRecognizer(appContext, activityHolder, audioPermissionRequester);
+ }
+
+ private native boolean nativeInitialize();
+
+ protected void onActivityStart(Activity activity, KeyboardEditor keyboardEditor) {
+ activityHolder.set(activity);
+ this.keyboardEditor = keyboardEditor;
+ }
+
+ protected void onActivityStop(Activity activity) {
+ if (activityHolder.get() == activity) {
+ activityHolder.set(null);
+ }
+ }
+
+ protected void onActivityDestroy(Activity activity) {
+ if (starboardStopped) {
+ // We can't restart the starboard app, so kill the process for a clean start next time.
+ Log.i(TAG, "Activity destroyed after shutdown; killing app.");
+ System.exit(0);
+ } else {
+ Log.i(TAG, "Activity destroyed without shutdown; app suspended in background.");
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ void beforeStartOrResume() {
+ Log.i(TAG, "Prepare to resume");
+ // Bring our platform services to life before resuming so that they're ready to deal with
+ // whatever the web app wants to do with them as part of its start/resume logic.
+ cobaltMediaSession.resume();
+ feedbackService.connect();
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ void beforeSuspend() {
+ Log.i(TAG, "Prepare to suspend");
+ // We want the MediaSession to be deactivated immediately before suspending so that by the time
+ // the launcher is visible our "Now Playing" card is already gone. Then Cobalt and the web app
+ // can take their time suspending after that.
+ cobaltMediaSession.suspend();
+ feedbackService.disconnect();
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ void afterStopped() {
+ starboardStopped = true;
+ ttsHelper.shutdown();
+ userAuthorizer.shutdown();
+ Activity activity = activityHolder.get();
+ if (activity != null) {
+ // Wait until the activity is destroyed to exit.
+ Log.i(TAG, "Shutdown in foreground; finishing Activity and removing task.");
+ activity.finishAndRemoveTask();
+ } else {
+ // We can't restart the starboard app, so kill the process for a clean start next time.
+ Log.i(TAG, "Shutdown in background; killing app without removing task.");
+ System.exit(0);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ public void requestStop(int errorLevel) {
+ if (!starboardStopped) {
+ nativeStopApp(errorLevel);
+ }
+ }
+
+ private native void nativeStopApp(int errorLevel);
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ public void requestSuspend() {
+ Activity activity = activityHolder.get();
+ if (activity != null) {
+ activity.finish();
+ }
+ }
+
+ public boolean onSearchRequested() {
+ return nativeOnSearchRequested();
+ }
+
+ private native boolean nativeOnSearchRequested();
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ public Context getApplicationContext() {
+ return appContext;
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ PlatformError raisePlatformError(@PlatformError.ErrorType int errorType, long data) {
+ PlatformError error = new PlatformError(activityHolder, errorType, data);
+ error.raise();
+ return error;
+ }
+
+ /** Returns true if the native code is compiled for release (i.e. 'gold' build). */
+ public static boolean isReleaseBuild() {
+ return nativeIsReleaseBuild();
+ }
+
+ private static native boolean nativeIsReleaseBuild();
+
+ protected Holder<Activity> getActivityHolder() {
+ return activityHolder;
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ protected String[] getArgs() {
+ return args;
+ }
+
+ /** Returns the URL from the Intent that started the app. */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ protected String getStartDeepLink() {
+ return startDeepLink;
+ }
+
+ /** Sends an event to the web app to navigate to the given URL */
+ public void handleDeepLink(String url) {
+ nativeHandleDeepLink(url);
+ }
+
+ private native void nativeHandleDeepLink(String url);
+
+ /**
+ * Returns the absolute path to the directory where application specific files should be written.
+ * May be overridden for use cases that need to segregate storage.
+ */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ protected String getFilesAbsolutePath() {
+ return appContext.getFilesDir().getAbsolutePath();
+ }
+
+ /**
+ * Returns the absolute path to the application specific cache directory on the filesystem. May be
+ * overridden for use cases that need to segregate storage.
+ */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ protected String getCacheAbsolutePath() {
+ return appContext.getCacheDir().getAbsolutePath();
+ }
+
+ /**
+ * Returns non-loopback network interface address, or null if none.
+ *
+ * <p>An IPv4 address will have only a 4 byte array, while an IPv6 address will have a 16 byte
+ * array.
+ *
+ * <p>A Java function to help implement Starboard's SbSocketGetLocalInterfaceAddress.
+ *
+ * <p>Required for platforms older than 24. Since 24, bionic includes getifaddrs() which can be
+ * used by the C layer directly.
+ */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ byte[] getLocalInterfaceAddress() {
+ try {
+ Enumeration<NetworkInterface> it = NetworkInterface.getNetworkInterfaces();
+
+ while (it.hasMoreElements()) {
+ NetworkInterface ni = it.nextElement();
+ if (ni.isLoopback()) {
+ continue;
+ }
+ if (!ni.isUp()) {
+ continue;
+ }
+ if (ni.isPointToPoint()) {
+ continue;
+ }
+
+ for (InterfaceAddress ia : ni.getInterfaceAddresses()) {
+ // Just return the first address.
+ return ia.getAddress().getAddress();
+ }
+ }
+ } catch (SocketException ex) {
+ // TODO should we have a logging story that strips logs for production?
+ Log.w(TAG, "sbSocketGetLocalInterfaceAddress exception", ex);
+ return null;
+ }
+ return null;
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ CobaltTextToSpeechHelper getTextToSpeechHelper() {
+ return ttsHelper;
+ }
+
+ /** @return A new CaptionSettings object with the current system caption settings. */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ CaptionSettings getCaptionSettings() {
+ CaptioningManager cm =
+ (CaptioningManager) appContext.getSystemService(Context.CAPTIONING_SERVICE);
+ return new CaptionSettings(cm);
+ }
+
+ /** Java-layer implementation of SbSystemGetLocaleId. */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ String systemGetLocaleId() {
+ return Locale.getDefault().toLanguageTag();
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ Size getDisplaySize() {
+ return DisplayUtil.getSystemDisplaySize(appContext);
+ }
+
+ /**
+ * Checks if there is no microphone connected to the system.
+ *
+ * @return true if no device is connected.
+ */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ public boolean isMicrophoneDisconnected() {
+ if (Build.VERSION.SDK_INT >= 23) {
+ return !isMicrophoneConnectedV23();
+ } else {
+ // There is no way of checking for a connected microphone/device before API 23, so cannot
+ // guarantee that no microphone is connected.
+ return false;
+ }
+ }
+
+ @TargetApi(23)
+ private boolean isMicrophoneConnectedV23() {
+ // A check specifically for microphones is not available before API 28, so it is assumed that a
+ // connected input audio device is a microphone.
+ AudioManager audioManager = (AudioManager) appContext.getSystemService(AUDIO_SERVICE);
+ AudioDeviceInfo[] devices = audioManager.getDevices(GET_DEVICES_INPUTS);
+ return devices.length > 0;
+ }
+
+ /**
+ * Checks if the microphone is muted.
+ *
+ * @return true if the microphone mute is on.
+ */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ public boolean isMicrophoneMute() {
+ AudioManager audioManager = (AudioManager) appContext.getSystemService(AUDIO_SERVICE);
+ return audioManager.isMicrophoneMute();
+ }
+
+ /** @return true if we have an active network connection and it's on a wireless network. */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ boolean isCurrentNetworkWireless() {
+ ConnectivityManager connMgr =
+ (ConnectivityManager) appContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+
+ NetworkInfo activeInfo = connMgr.getActiveNetworkInfo();
+
+ if (activeInfo == null) {
+ return false;
+ }
+
+ switch (activeInfo.getType()) {
+ case ConnectivityManager.TYPE_ETHERNET:
+ return false;
+ default:
+ // Consider anything that's not definitely wired to be wireless.
+ // For example, TYPE_VPN is ambiguous, but it's highly likely to be
+ // over wifi.
+ return true;
+ }
+ }
+
+ /**
+ * @return true if the user has enabled accessibility high contrast text in the operating system.
+ */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ boolean isAccessibilityHighContrastTextEnabled() {
+ AccessibilityManager am =
+ (AccessibilityManager) appContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
+
+ try {
+ Method m = AccessibilityManager.class.getDeclaredMethod("isHighTextContrastEnabled");
+
+ return m.invoke(am) == Boolean.TRUE;
+ } catch (ReflectiveOperationException ex) {
+ return false;
+ }
+ }
+
+ /** Returns Java layer implementation for AndroidUserAuthorizer */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ UserAuthorizer getUserAuthorizer() {
+ return userAuthorizer;
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ void sendFeedback(
+ HashMap<String, String> productSpecificData, String categoryTag, byte[] screenshotData) {
+ // Convert the screenshot byte array into a Bitmap.
+ Bitmap screenshotBitmap = null;
+ if ((screenshotData != null) && (screenshotData.length > 0)) {
+ screenshotBitmap = BitmapFactory.decodeByteArray(screenshotData, 0, screenshotData.length);
+ if (screenshotBitmap == null) {
+ Log.e(TAG, "Unable to decode a screenshot from the data.");
+ }
+ }
+ feedbackService.sendFeedback(productSpecificData, categoryTag, screenshotBitmap);
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ void updateMediaSession(
+ int playbackState,
+ long actions,
+ long positionMs,
+ float speed,
+ String title,
+ String artist,
+ String album,
+ MediaImage[] artwork) {
+ cobaltMediaSession.updateMediaSession(
+ playbackState, actions, positionMs, speed, title, artist, album, artwork);
+ }
+
+ /** Returns string for kSbSystemPropertyUserAgentAuxField */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ String getUserAgentAuxField() {
+ StringBuilder sb = new StringBuilder();
+
+ String packageName = appContext.getApplicationInfo().packageName;
+ sb.append(packageName);
+ sb.append('/');
+
+ try {
+ sb.append(appContext.getPackageManager().getPackageInfo(packageName, 0).versionName);
+ } catch (PackageManager.NameNotFoundException ex) {
+ // Should never happen
+ Log.e(TAG, "Can't find our own package", ex);
+ }
+
+ return sb.toString();
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ AudioOutputManager getAudioOutputManager() {
+ return audioOutputManager;
+ }
+
+ /** Returns Java layer implementation for KeyboardEditor */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ KeyboardEditor getKeyboardEditor() {
+ return keyboardEditor;
+ }
+
+ /** Returns Java layer implementation for AndroidVoiceRecognizer */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ VoiceRecognizer getVoiceRecognizer() {
+ return voiceRecognizer;
+ }
+
+ /** Returns Java layer implementation for AudioPermissionRequester */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ AudioPermissionRequester getAudioPermissionRequester() {
+ return audioPermissionRequester;
+ }
+
+ void onActivityResult(int requestCode, int resultCode, Intent data) {
+ userAuthorizer.onActivityResult(requestCode, resultCode, data);
+ }
+
+ void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+ userAuthorizer.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ audioPermissionRequester.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ public void setVideoSurfaceBounds(final int x, final int y, final int width, final int height) {
+ Activity activity = activityHolder.get();
+ if (activity instanceof CobaltActivity) {
+ ((CobaltActivity) activity).setVideoSurfaceBounds(x, y, width, height);
+ }
+ }
+
+ /**
+ * Check if hdrType is supported by the current default display. See
+ * https://developer.android.com/reference/android/view/Display.HdrCapabilities.html for valid
+ * values.
+ */
+ @TargetApi(24)
+ @SuppressWarnings("unused")
+ @UsedByNative
+ public boolean isHdrTypeSupported(int hdrType) {
+ if (android.os.Build.VERSION.SDK_INT < 24) {
+ return false;
+ }
+
+ Activity activity = activityHolder.get();
+ if (activity == null) {
+ return false;
+ }
+
+ WindowManager windowManager = activity.getWindowManager();
+ if (windowManager == null) {
+ return false;
+ }
+
+ int[] supportedHdrTypes =
+ windowManager.getDefaultDisplay().getHdrCapabilities().getSupportedHdrTypes();
+ for (int supportedType : supportedHdrTypes) {
+ if (supportedType == hdrType) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/VoiceRecognizer.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/VoiceRecognizer.java
new file mode 100644
index 0000000..d9aba31
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/VoiceRecognizer.java
@@ -0,0 +1,193 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.coat;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.speech.RecognitionListener;
+import android.speech.RecognizerIntent;
+import android.speech.SpeechRecognizer;
+import dev.cobalt.util.Holder;
+import dev.cobalt.util.UsedByNative;
+import java.util.ArrayList;
+
+/**
+ * This class uses Android's SpeechRecognizer to perform speech recognition. Using Android's
+ * platform recognizer offers several benefits such as good quality and good local fallback when no
+ * data connection is available.
+ */
+public class VoiceRecognizer {
+ private final Context context;
+ private final Holder<Activity> activityHolder;
+ private final Handler mainHandler = new Handler(Looper.getMainLooper());
+ private final AudioPermissionRequester audioPermissionRequester;
+ private SpeechRecognizer speechRecognizer;
+
+ // Native pointer to C++ SbSpeechRecognizerImpl.
+ private long nativeSpeechRecognizerImpl;
+
+ // Remember if we are using continuous recognition.
+ private boolean continuous;
+ private boolean interimResults;
+ private int maxAlternatives;
+
+ // Internal class to handle events from Android's SpeechRecognizer and route
+ // them to native.
+ class Listener implements RecognitionListener {
+ @Override
+ public void onBeginningOfSpeech() {
+ nativeOnSpeechDetected(nativeSpeechRecognizerImpl, true);
+ }
+
+ @Override
+ public void onBufferReceived(byte[] buffer) {}
+
+ @Override
+ public void onEndOfSpeech() {
+ nativeOnSpeechDetected(nativeSpeechRecognizerImpl, false);
+ }
+
+ @Override
+ public void onError(int error) {
+ nativeOnError(nativeSpeechRecognizerImpl, error);
+ reset();
+ }
+
+ @Override
+ public void onEvent(int eventType, Bundle params) {}
+
+ @Override
+ public void onPartialResults(Bundle bundle) {
+ handleResults(bundle, false);
+ }
+
+ @Override
+ public void onReadyForSpeech(Bundle params) {}
+
+ @Override
+ public void onResults(Bundle bundle) {
+ handleResults(bundle, true);
+ reset();
+ }
+
+ @Override
+ public void onRmsChanged(float rmsdB) {}
+
+ private void handleResults(Bundle bundle, boolean isFinal) {
+ ArrayList<String> list = bundle.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);
+ String[] results = list.toArray(new String[list.size()]);
+ float[] scores = bundle.getFloatArray(SpeechRecognizer.CONFIDENCE_SCORES);
+ nativeOnResults(nativeSpeechRecognizerImpl, results, scores, isFinal);
+ }
+ };
+
+ public VoiceRecognizer(
+ Context context,
+ Holder<Activity> activityHolder,
+ AudioPermissionRequester audioPermissionRequester) {
+ this.context = context;
+ this.activityHolder = activityHolder;
+ this.audioPermissionRequester = audioPermissionRequester;
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ public void startRecognition(
+ boolean continuous,
+ boolean interimResults,
+ int maxAlternatives,
+ long nativeSpeechRecognizer) {
+ this.continuous = continuous;
+ this.interimResults = interimResults;
+ this.maxAlternatives = maxAlternatives;
+ this.nativeSpeechRecognizerImpl = nativeSpeechRecognizer;
+
+ if (this.audioPermissionRequester.requestRecordAudioPermission(
+ this.nativeSpeechRecognizerImpl)) {
+ startRecognitionInternal();
+ } else {
+ mainHandler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ nativeOnError(
+ nativeSpeechRecognizerImpl, SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS);
+ }
+ });
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ public void stopRecognition() {
+ Runnable runnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ if (Looper.myLooper() != Looper.getMainLooper()) {
+ throw new RuntimeException("Must be called in main thread.");
+ }
+ if (speechRecognizer == null) {
+ return;
+ }
+ reset();
+ }
+ };
+ mainHandler.post(runnable);
+ }
+
+ private void startRecognitionInternal() {
+ Runnable runnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ if (Looper.myLooper() != Looper.getMainLooper()) {
+ throw new RuntimeException("Must be called in main thread.");
+ }
+ speechRecognizer = SpeechRecognizer.createSpeechRecognizer(context);
+ speechRecognizer.setRecognitionListener(new Listener());
+ Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
+ intent.putExtra(
+ RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
+ intent.putExtra("android.speech.extra.DICTATION_MODE", continuous);
+ intent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, interimResults);
+ intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxAlternatives);
+ speechRecognizer.startListening(intent);
+ }
+ };
+ mainHandler.post(runnable);
+ }
+
+ private void reset() {
+ speechRecognizer.destroy();
+ speechRecognizer = null;
+
+ nativeSpeechRecognizerImpl = 0;
+ continuous = false;
+ interimResults = false;
+ maxAlternatives = 1;
+ }
+
+ private native void nativeOnSpeechDetected(long nativeSpeechRecognizerImpl, boolean detected);
+
+ private native void nativeOnError(long nativeSpeechRecognizerImpl, int error);
+
+ private native void nativeOnResults(
+ long nativeSpeechRecognizerImpl, String[] results, float[] confidences, boolean isFinal);
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/feedback/FeedbackService.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/feedback/FeedbackService.java
new file mode 100644
index 0000000..66a2547
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/feedback/FeedbackService.java
@@ -0,0 +1,36 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.feedback;
+
+import android.graphics.Bitmap;
+import dev.cobalt.util.UsedByNative;
+import java.util.HashMap;
+
+/** Java side implementation for starboard::android::shared::cobalt::FeedbackService. */
+public interface FeedbackService {
+ /**
+ * Sends the given product specific data to the GMS Feedback Service.
+ *
+ * <p>Implementations must annotate this method with @UsedByNative so Proguard doesn't remove it.
+ */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ void sendFeedback(
+ HashMap<String, String> productSpecificData, String categoryTag, Bitmap screenshot);
+
+ void connect();
+
+ void disconnect();
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/feedback/NoopFeedbackService.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/feedback/NoopFeedbackService.java
new file mode 100644
index 0000000..0c59c84
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/feedback/NoopFeedbackService.java
@@ -0,0 +1,49 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.feedback;
+
+import static dev.cobalt.util.Log.TAG;
+
+import android.graphics.Bitmap;
+import android.text.TextUtils;
+import dev.cobalt.util.Log;
+import dev.cobalt.util.UsedByNative;
+import java.util.HashMap;
+
+/** FeedbackService implementation that doesn't send any feedback. */
+public class NoopFeedbackService implements FeedbackService {
+ @Override
+ @SuppressWarnings("unused")
+ @UsedByNative
+ public void sendFeedback(
+ HashMap<String, String> productSpecificData, String categoryTag, Bitmap screenshot) {
+ Log.i(TAG, "Feedback product specific data:");
+ for (String key : productSpecificData.keySet()) {
+ Log.i(TAG, key + ": " + productSpecificData.get(key));
+ }
+ if (screenshot != null) {
+ Log.i(TAG, "Screenshot dimensions: " + screenshot.getWidth() + "x" + screenshot.getHeight());
+ }
+ if (!TextUtils.isEmpty(categoryTag)) {
+ Log.i(TAG, "Category tag: " + categoryTag);
+ }
+ }
+
+ @Override
+ public void connect() {}
+
+ @Override
+ public void disconnect() {}
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/ArtworkLoader.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/ArtworkLoader.java
new file mode 100644
index 0000000..d76266f
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/ArtworkLoader.java
@@ -0,0 +1,167 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.media;
+
+import static dev.cobalt.media.Log.TAG;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.AsyncTask;
+import android.support.annotation.NonNull;
+import android.util.Pair;
+import android.util.Size;
+import dev.cobalt.util.Log;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+/** Loads MediaImage artwork, and caches one image. */
+public class ArtworkLoader {
+
+ /** Callback to receive the image loaded in the background by getOrLoadArtwork() */
+ public interface Callback {
+ void onArtworkLoaded(Bitmap bitmap);
+ }
+
+ @NonNull private volatile String requestedArtworkUrl = "";
+ @NonNull private volatile String currentArtworkUrl = "";
+ private volatile Bitmap currentArtwork = null;
+
+ private final Callback callback;
+ private final Size displaySize;
+
+ public ArtworkLoader(Callback callback, Size displaySize) {
+ this.callback = callback;
+ this.displaySize = displaySize;
+ }
+
+ /**
+ * Returns a cached image if available. If not cached, returns null and starts downloading it in
+ * the background, and then when ready the callback will be called with the image.
+ */
+ public synchronized Bitmap getOrLoadArtwork(MediaImage[] artwork) {
+ MediaImage image = getBestFitImage(artwork);
+ String url = (image == null) ? "" : image.src;
+
+ // Check if this artwork is already loaded or requested.
+ if (url.equals(currentArtworkUrl)) {
+ return currentArtwork;
+ } else if (url.equals(requestedArtworkUrl)) {
+ return null;
+ }
+
+ requestedArtworkUrl = url;
+ new DownloadArtworkTask().execute(url);
+ return null;
+ }
+
+ /**
+ * Returns the image that most closely matches the display size, or null if there are no images.
+ * We don't really know what size view the image may appear in (on the Now Playing card on Android
+ * TV launcher, or any other observer of the MediaSession), so we use display size as the largest
+ * useful size on any particular device.
+ */
+ private MediaImage getBestFitImage(MediaImage[] artwork) {
+ if (artwork == null || artwork.length == 0) {
+ return null;
+ }
+ MediaImage bestImage = artwork[0];
+ int minDiagonalSquared = Integer.MAX_VALUE;
+ for (MediaImage image : artwork) {
+ Size imageSize = parseImageSize(image);
+ int widthDelta = displaySize.getWidth() - imageSize.getWidth();
+ int heightDelta = displaySize.getHeight() - imageSize.getHeight();
+ int diagonalSquared = widthDelta * widthDelta + heightDelta * heightDelta;
+ if (diagonalSquared < minDiagonalSquared) {
+ bestImage = image;
+ minDiagonalSquared = diagonalSquared;
+ }
+ }
+ return bestImage;
+ }
+
+ private Size parseImageSize(MediaImage image) {
+ try {
+ String sizeStr = image.sizes.split("\\s+", -1)[0];
+ return Size.parseSize(sizeStr.toLowerCase());
+ } catch (NumberFormatException | NullPointerException e) {
+ return new Size(0, 0);
+ }
+ }
+
+ private synchronized void onDownloadFinished(Pair<String, Bitmap> urlBitmapPair) {
+ String url = urlBitmapPair.first;
+ Bitmap bitmap = urlBitmapPair.second;
+ if (url.equals(requestedArtworkUrl)) {
+ requestedArtworkUrl = "";
+ if (bitmap != null) {
+ currentArtworkUrl = url;
+ currentArtwork = bitmap;
+ callback.onArtworkLoaded(bitmap);
+ }
+ }
+ }
+
+ private class DownloadArtworkTask extends AsyncTask<String, Void, Pair<String, Bitmap>> {
+
+ @Override
+ protected Pair<String, Bitmap> doInBackground(String... params) {
+ String url = params[0];
+ Bitmap bitmap = null;
+ HttpURLConnection conn = null;
+ InputStream is = null;
+ try {
+ conn = (HttpURLConnection) new URL(url).openConnection();
+ is = conn.getInputStream();
+ bitmap = BitmapFactory.decodeStream(is);
+ } catch (IOException e) {
+ Log.e(TAG, "Could not download artwork", e);
+ } finally {
+ try {
+ if (conn != null) {
+ conn.disconnect();
+ }
+ if (is != null) {
+ is.close();
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error closing connection for artwork", e);
+ }
+ }
+
+ // Crop to 16:9 as needed
+ if (bitmap != null) {
+ int height = bitmap.getWidth() * 9 / 16;
+ if (bitmap.getHeight() > height) {
+ int top = (bitmap.getHeight() - height) / 2;
+ bitmap = Bitmap.createBitmap(bitmap, 0, top, bitmap.getWidth(), height);
+ }
+ }
+
+ return Pair.create(url, bitmap);
+ }
+
+ @Override
+ protected void onPostExecute(Pair<String, Bitmap> urlBitmapPair) {
+ onDownloadFinished(urlBitmapPair);
+ }
+
+ @Override
+ protected void onCancelled(Pair<String, Bitmap> urlBitmapPair) {
+ onDownloadFinished(urlBitmapPair);
+ }
+ }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioOutputManager.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioOutputManager.java
new file mode 100644
index 0000000..0a72bc8
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioOutputManager.java
@@ -0,0 +1,108 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.media;
+
+import static dev.cobalt.media.Log.TAG;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.os.Build;
+import dev.cobalt.util.Log;
+import dev.cobalt.util.UsedByNative;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Creates and destroys AudioTrackBridge and handles the volume change. */
+public class AudioOutputManager implements CobaltMediaSession.UpdateVolumeListener {
+ private List<AudioTrackBridge> audioTrackBridgeList;
+ private Context context;
+
+ public AudioOutputManager(Context context) {
+ this.context = context;
+ audioTrackBridgeList = new ArrayList<AudioTrackBridge>();
+ }
+
+ @Override
+ public void onUpdateVolume(float gain) {
+ for (AudioTrackBridge audioTrackBridge : audioTrackBridgeList) {
+ audioTrackBridge.setVolume(gain);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ AudioTrackBridge createAudioTrackBridge(
+ int sampleType, int sampleRate, int channelCount, int framesPerChannel) {
+ AudioTrackBridge audioTrackBridge =
+ new AudioTrackBridge(sampleType, sampleRate, channelCount, framesPerChannel);
+ if (!audioTrackBridge.isAudioTrackValid()) {
+ Log.e(TAG, "AudioTrackBridge has invalid audio track");
+ return null;
+ }
+ audioTrackBridgeList.add(audioTrackBridge);
+ return audioTrackBridge;
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ void destroyAudioTrackBridge(AudioTrackBridge audioTrackBridge) {
+ audioTrackBridge.release();
+ audioTrackBridgeList.remove(audioTrackBridge);
+ }
+
+ /** Returns the maximum number of HDMI channels. */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ int getMaxChannels() {
+ // The aac audio decoder on this platform will switch its output from 5.1
+ // to stereo right before providing the first output buffer when
+ // attempting to decode 5.1 input. Since this heavily violates invariants
+ // of the shared starboard player framework, disable 5.1 on this platform.
+ // It is expected that we will be able to resolve this issue with Xiaomi
+ // by Android P, so only do this workaround for SDK versions < 27.
+ if (android.os.Build.MODEL.equals("MIBOX3") && android.os.Build.VERSION.SDK_INT < 27) {
+ return 2;
+ }
+
+ if (Build.VERSION.SDK_INT >= 23) {
+ return getMaxChannelsV23();
+ }
+ return 2;
+ }
+
+ /** Returns the maximum number of HDMI channels for API 23 and above. */
+ @TargetApi(23)
+ private int getMaxChannelsV23() {
+ AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ AudioDeviceInfo[] deviceInfos = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
+ int maxChannels = 2;
+ for (AudioDeviceInfo info : deviceInfos) {
+ int type = info.getType();
+ if (type == AudioDeviceInfo.TYPE_HDMI || type == AudioDeviceInfo.TYPE_HDMI_ARC) {
+ int[] channelCounts = info.getChannelCounts();
+ if (channelCounts.length == 0) {
+ // An empty array indicates that the device supports arbitrary channel masks.
+ return 8;
+ }
+ for (int count : channelCounts) {
+ maxChannels = Math.max(maxChannels, count);
+ }
+ }
+ }
+ return maxChannels;
+ }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioTrackBridge.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioTrackBridge.java
new file mode 100644
index 0000000..54d00ac
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioTrackBridge.java
@@ -0,0 +1,204 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.media;
+
+import static dev.cobalt.media.Log.TAG;
+
+import android.media.AudioAttributes;
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioTimestamp;
+import android.media.AudioTrack;
+import android.os.Build;
+import dev.cobalt.util.Log;
+import dev.cobalt.util.UsedByNative;
+import java.nio.ByteBuffer;
+
+/** A wrapper of the android AudioTrack class. */
+@UsedByNative
+public class AudioTrackBridge {
+ private AudioTrack audioTrack;
+ private AudioTimestamp audioTimestamp = new AudioTimestamp();
+ private long maxFramePositionSoFar = 0;
+
+ public AudioTrackBridge(int sampleType, int sampleRate, int channelCount, int framesPerChannel) {
+ int channelConfig;
+ switch (channelCount) {
+ case 1:
+ channelConfig = AudioFormat.CHANNEL_OUT_MONO;
+ break;
+ case 2:
+ channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
+ break;
+ case 6:
+ channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
+ break;
+ default:
+ throw new RuntimeException("Unsupported channel count: " + channelCount);
+ }
+
+ AudioAttributes attributes =
+ new AudioAttributes.Builder()
+ .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+ .setUsage(AudioAttributes.USAGE_MEDIA)
+ .build();
+ AudioFormat format =
+ new AudioFormat.Builder()
+ .setEncoding(sampleType)
+ .setSampleRate(sampleRate)
+ .setChannelMask(channelConfig)
+ .build();
+
+ int minBufferSizeBytes = AudioTrack.getMinBufferSize(sampleRate, channelConfig, sampleType);
+ int audioTrackBufferSize = minBufferSizeBytes;
+ // Use framesPerChannel to determine the buffer size. To use a large buffer on a small
+ // framesPerChannel may lead to audio playback not able to start.
+ while (audioTrackBufferSize < framesPerChannel) {
+ audioTrackBufferSize *= 2;
+ }
+ while (audioTrackBufferSize > 0) {
+ try {
+ audioTrack =
+ new AudioTrack(
+ attributes,
+ format,
+ audioTrackBufferSize,
+ AudioTrack.MODE_STREAM,
+ AudioManager.AUDIO_SESSION_ID_GENERATE);
+ } catch (Exception e) {
+ audioTrack = null;
+ }
+ // AudioTrack ctor can fail in multiple, platform specific ways, so do a thorough check
+ // before proceed.
+ if (audioTrack != null && audioTrack.getState() == AudioTrack.STATE_INITIALIZED) {
+ break;
+ }
+ audioTrackBufferSize /= 2;
+ }
+ Log.i(
+ TAG,
+ String.format(
+ "AudioTrack created with buffer size %d. The minimum buffer size is %d.",
+ audioTrackBufferSize, minBufferSizeBytes));
+ }
+
+ public Boolean isAudioTrackValid() {
+ return audioTrack != null;
+ }
+
+ public void release() {
+ if (audioTrack != null) {
+ audioTrack.release();
+ }
+ audioTrack = null;
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ public int setVolume(float gain) {
+ if (audioTrack == null) {
+ Log.e(TAG, "Unable to setVolume with NULL audio track.");
+ return 0;
+ }
+ return audioTrack.setVolume(gain);
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private void play() {
+ if (audioTrack == null) {
+ Log.e(TAG, "Unable to play with NULL audio track.");
+ return;
+ }
+ audioTrack.play();
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private void pause() {
+ if (audioTrack == null) {
+ Log.e(TAG, "Unable to pause with NULL audio track.");
+ return;
+ }
+ audioTrack.pause();
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private void flush() {
+ if (audioTrack == null) {
+ Log.e(TAG, "Unable to flush with NULL audio track.");
+ return;
+ }
+ audioTrack.flush();
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private int write(byte[] audioData, int sizeInBytes) {
+ if (audioTrack == null) {
+ Log.e(TAG, "Unable to write with NULL audio track.");
+ return 0;
+ }
+ if (Build.VERSION.SDK_INT >= 23) {
+ return audioTrack.write(audioData, 0, sizeInBytes, AudioTrack.WRITE_NON_BLOCKING);
+ } else {
+ ByteBuffer byteBuffer = ByteBuffer.wrap(audioData);
+ return audioTrack.write(byteBuffer, sizeInBytes, AudioTrack.WRITE_NON_BLOCKING);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private int write(float[] audioData, int sizeInFloats) {
+ if (audioTrack == null) {
+ Log.e(TAG, "Unable to write with NULL audio track.");
+ return 0;
+ }
+ return audioTrack.write(audioData, 0, sizeInFloats, AudioTrack.WRITE_NON_BLOCKING);
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private AudioTimestamp getAudioTimestamp() {
+ // TODO: Consider calling with TIMEBASE_MONOTONIC and returning that
+ // information to the starboard audio sink.
+ if (audioTrack == null) {
+ Log.e(TAG, "Unable to getAudioTimestamp with NULL audio track.");
+ return audioTimestamp;
+ }
+ if (audioTrack.getTimestamp(audioTimestamp)) {
+ // This conversion is safe, as only the lower bits will be set, since we
+ // called |getTimestamp| without a timebase.
+ // https://developer.android.com/reference/android/media/AudioTimestamp.html#framePosition
+ audioTimestamp.framePosition = (int) audioTimestamp.framePosition;
+ } else {
+ // Time stamps haven't been updated yet, assume playback hasn't started.
+ audioTimestamp.framePosition = 0;
+ audioTimestamp.nanoTime = System.nanoTime();
+ }
+
+ // TODO: This is required for correctness of the audio sink, because
+ // otherwise we would be going back in time. Investigate the impact it has
+ // on playback. All empirical measurements so far suggest that it should
+ // be negligible.
+ if (audioTimestamp.framePosition < maxFramePositionSoFar) {
+ audioTimestamp.framePosition = maxFramePositionSoFar;
+ }
+ maxFramePositionSoFar = audioTimestamp.framePosition;
+
+ return audioTimestamp;
+ }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/CaptionSettings.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/CaptionSettings.java
new file mode 100644
index 0000000..43d6997
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/CaptionSettings.java
@@ -0,0 +1,49 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.media;
+
+import android.view.accessibility.CaptioningManager;
+import dev.cobalt.util.UsedByNative;
+
+/**
+ * Captures the system Caption style in properties as needed by the Starboard implementation.
+ */
+public class CaptionSettings {
+
+ @UsedByNative public final boolean isEnabled;
+ @UsedByNative public final float fontScale;
+ @UsedByNative public final int edgeType;
+ @UsedByNative public final boolean hasEdgeType;
+ @UsedByNative public final int foregroundColor;
+ @UsedByNative public final boolean hasForegroundColor;
+ @UsedByNative public final int backgroundColor;
+ @UsedByNative public final boolean hasBackgroundColor;
+ @UsedByNative public final int windowColor;
+ @UsedByNative public final boolean hasWindowColor;
+
+ public CaptionSettings(CaptioningManager cm) {
+ CaptioningManager.CaptionStyle style = cm.getUserStyle();
+ isEnabled = cm.isEnabled();
+ fontScale = cm.getFontScale();
+ edgeType = style.edgeType;
+ hasEdgeType = style.hasEdgeType();
+ foregroundColor = style.foregroundColor;
+ hasForegroundColor = style.hasForegroundColor();
+ backgroundColor = style.backgroundColor;
+ hasBackgroundColor = style.hasBackgroundColor();
+ windowColor = style.windowColor;
+ hasWindowColor = style.hasWindowColor();
+ }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/CobaltMediaSession.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/CobaltMediaSession.java
new file mode 100644
index 0000000..8ca2a31
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/CobaltMediaSession.java
@@ -0,0 +1,440 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.media;
+
+import static dev.cobalt.media.Log.TAG;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.media.AudioAttributes;
+import android.media.AudioAttributes.Builder;
+import android.media.AudioFocusRequest;
+import android.media.AudioManager;
+import android.media.MediaMetadata;
+import android.media.session.MediaSession;
+import android.media.session.PlaybackState;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.WindowManager;
+import dev.cobalt.util.DisplayUtil;
+import dev.cobalt.util.Holder;
+import dev.cobalt.util.Log;
+
+/**
+ * Cobalt MediaSession glue, as well as collection of state and logic to switch on/off Android OS
+ * features used in media playback, such as audio focus, "KEEP_SCREEN_ON" mode, and "visible
+ * behind".
+ */
+public class CobaltMediaSession
+ implements AudioManager.OnAudioFocusChangeListener, ArtworkLoader.Callback {
+
+ // We do handle transport controls and set this flag on all API levels, even though it's
+ // deprecated and unnecessary on API 26+.
+ @SuppressWarnings("deprecation")
+ private static final int MEDIA_SESSION_FLAG_HANDLES_TRANSPORT_CONTROLS =
+ MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS;
+
+ private AudioFocusRequest audioFocusRequest;
+
+ interface UpdateVolumeListener {
+ /** Called when there is a change in audio focus. */
+ void onUpdateVolume(float gain);
+ }
+
+ /**
+ * When losing audio focus with the option of ducking, we reduce the volume to 10%. This arbitrary
+ * number is what YouTube Android Player infrastructure uses.
+ */
+ private static final float AUDIO_FOCUS_DUCK_LEVEL = 0.1f;
+
+ private final Handler mainHandler = new Handler(Looper.getMainLooper());
+
+ private final Context context;
+ private final Holder<Activity> activityHolder;
+
+ private final UpdateVolumeListener volumeListener;
+ private final ArtworkLoader artworkLoader;
+ private MediaSession mediaSession;
+
+ // We re-use the builder to hold onto the most recent metadata and add artwork later.
+ private MediaMetadata.Builder metadataBuilder = new MediaMetadata.Builder();
+ // We re-use the builder to hold onto the most recent playback state.
+ private PlaybackState.Builder playbackStateBuilder = new PlaybackState.Builder();
+
+ // Duplicated in starboard/android/shared/android_media_session_client.h
+ // PlaybackState
+ private static final int PLAYBACK_STATE_PLAYING = 0;
+ private static final int PLAYBACK_STATE_PAUSED = 1;
+ private static final int PLAYBACK_STATE_NONE = 2;
+ private static final String[] PLAYBACK_STATE_NAME = {"playing", "paused", "none"};
+
+ // Accessed on the main looper thread only.
+ private int playbackState = PLAYBACK_STATE_NONE;
+ private boolean transientPause = false;
+ private boolean suspended = true;
+
+ public CobaltMediaSession(
+ Context context, Holder<Activity> activityHolder, UpdateVolumeListener volumeListener) {
+ this.context = context;
+ this.activityHolder = activityHolder;
+
+ this.volumeListener = volumeListener;
+ artworkLoader = new ArtworkLoader(this, DisplayUtil.getDisplaySize(context));
+ setMediaSession();
+ }
+
+ private void setMediaSession() {
+ mediaSession = new MediaSession(context, TAG);
+ mediaSession.setFlags(MEDIA_SESSION_FLAG_HANDLES_TRANSPORT_CONTROLS);
+ mediaSession.setCallback(
+ new MediaSession.Callback() {
+ @Override
+ public void onFastForward() {
+ Log.i(TAG, "MediaSession action: FAST FORWARD");
+ nativeInvokeAction(PlaybackState.ACTION_FAST_FORWARD);
+ }
+
+ @Override
+ public void onPause() {
+ Log.i(TAG, "MediaSession action: PAUSE");
+ nativeInvokeAction(PlaybackState.ACTION_PAUSE);
+ }
+
+ @Override
+ public void onPlay() {
+ Log.i(TAG, "MediaSession action: PLAY");
+ nativeInvokeAction(PlaybackState.ACTION_PLAY);
+ }
+
+ @Override
+ public void onRewind() {
+ Log.i(TAG, "MediaSession action: REWIND");
+ nativeInvokeAction(PlaybackState.ACTION_REWIND);
+ }
+
+ @Override
+ public void onSkipToNext() {
+ Log.i(TAG, "MediaSession action: SKIP NEXT");
+ nativeInvokeAction(PlaybackState.ACTION_SKIP_TO_NEXT);
+ }
+
+ @Override
+ public void onSkipToPrevious() {
+ Log.i(TAG, "MediaSession action: SKIP PREVIOUS");
+ nativeInvokeAction(PlaybackState.ACTION_SKIP_TO_PREVIOUS);
+ }
+
+ @Override
+ public void onSeekTo(long pos) {
+ Log.i(TAG, "MediaSession action: SEEK " + pos);
+ nativeInvokeAction(PlaybackState.ACTION_SEEK_TO, pos);
+ }
+ });
+ // |metadataBuilder| may still have no fields at this point, yielding empty metadata.
+ mediaSession.setMetadata(metadataBuilder.build());
+ // |playbackStateBuilder| may still have no fields at this point.
+ mediaSession.setPlaybackState(playbackStateBuilder.build());
+ }
+
+ private static void checkMainLooperThread() {
+ if (Looper.getMainLooper() != Looper.myLooper()) {
+ throw new RuntimeException("Must be on main thread");
+ }
+ }
+
+ /**
+ * Sets system media resources active or not according to whether media is playing. This is
+ * idempotent as it may be called multiple times during the course of a media session.
+ */
+ private void configureMediaFocus(int playbackState) {
+ checkMainLooperThread();
+ if (transientPause && playbackState == PLAYBACK_STATE_PAUSED) {
+ Log.i(TAG, "Media focus: paused (transient)");
+ // Don't release media focus while transiently paused, otherwise we won't get audiofocus back
+ // when the transient condition ends and we would leave playback paused.
+ return;
+ }
+ Log.i(TAG, "Media focus: " + PLAYBACK_STATE_NAME[playbackState]);
+ wakeLock(playbackState == PLAYBACK_STATE_PLAYING);
+ audioFocus(playbackState == PLAYBACK_STATE_PLAYING);
+
+ boolean activating = playbackState != PLAYBACK_STATE_NONE && !mediaSession.isActive();
+ boolean deactivating = playbackState == PLAYBACK_STATE_NONE && mediaSession.isActive();
+ if (activating) {
+ // Resuming or new playbacks land here.
+ setMediaSession();
+ }
+ mediaSession.setActive(playbackState != PLAYBACK_STATE_NONE);
+ if (deactivating) {
+ // Suspending lands here.
+ mediaSession.release();
+ }
+ }
+
+ private void wakeLock(boolean lock) {
+ Activity activity = activityHolder.get();
+ if (activity == null) {
+ return;
+ }
+ if (lock) {
+ activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ } else {
+ activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+ }
+
+ private void audioFocus(boolean focus) {
+ if (focus) {
+ int res;
+ if (Build.VERSION.SDK_INT < 26) {
+ res = requestAudioFocus();
+ } else {
+ res = requestAudioFocusV26();
+ }
+ // This shouldn't happen, but pause playback to be nice if it does.
+ if (res != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+ Log.w(TAG, "Audiofocus action: PAUSE (not granted)");
+ nativeInvokeAction(PlaybackState.ACTION_PAUSE);
+ }
+ } else {
+ if (Build.VERSION.SDK_INT < 26) {
+ abandonAudioFocus();
+ } else {
+ abandonAudioFocusV26();
+ }
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ private int requestAudioFocus() {
+ return getAudioManager()
+ .requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
+ }
+
+ @TargetApi(26)
+ private int requestAudioFocusV26() {
+ if (audioFocusRequest == null) {
+ AudioAttributes audioAtrributes =
+ new Builder().setContentType(AudioAttributes.CONTENT_TYPE_MOVIE).build();
+ audioFocusRequest =
+ new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
+ .setOnAudioFocusChangeListener(this)
+ .setAudioAttributes(audioAtrributes)
+ .build();
+ }
+ return getAudioManager().requestAudioFocus(audioFocusRequest);
+ }
+
+ @SuppressWarnings("deprecation")
+ private void abandonAudioFocus() {
+ getAudioManager().abandonAudioFocus(this);
+ }
+
+ @TargetApi(26)
+ private void abandonAudioFocusV26() {
+ if (audioFocusRequest != null) {
+ getAudioManager().abandonAudioFocusRequest(audioFocusRequest);
+ }
+ }
+
+ /** AudioManager.OnAudioFocusChangeListener implementation. */
+ @Override
+ public void onAudioFocusChange(int focusChange) {
+ String logExtra = "";
+ switch (focusChange) {
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
+ logExtra = " (transient)";
+ // fall through
+ case AudioManager.AUDIOFOCUS_LOSS:
+ Log.i(TAG, "Audiofocus loss" + logExtra);
+ if (playbackState == PLAYBACK_STATE_PLAYING) {
+ Log.i(TAG, "Audiofocus action: PAUSE");
+ nativeInvokeAction(PlaybackState.ACTION_PAUSE);
+ }
+ break;
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
+ Log.i(TAG, "Audiofocus duck");
+ // Lower the volume, keep current play state.
+ // Starting with API 26 the system does automatic ducking without calling our listener,
+ // but we still need this for API < 26.
+ volumeListener.onUpdateVolume(AUDIO_FOCUS_DUCK_LEVEL);
+ break;
+ case AudioManager.AUDIOFOCUS_GAIN:
+ Log.i(TAG, "Audiofocus gain");
+ // The app has been granted audio focus (again). Raise volume to normal,
+ // restart playback if necessary.
+ volumeListener.onUpdateVolume(1.0f);
+ if (transientPause && playbackState == PLAYBACK_STATE_PAUSED) {
+ Log.i(TAG, "Audiofocus action: PLAY");
+ nativeInvokeAction(PlaybackState.ACTION_PLAY);
+ }
+ break;
+ default: // fall out
+ }
+
+ // Keep track of whether we're currently paused because of a transient loss of audiofocus.
+ transientPause = (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT);
+ }
+
+ private AudioManager getAudioManager() {
+ return (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ }
+
+ public void resume() {
+ mainHandler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ resumeInternal();
+ }
+ });
+ }
+
+ private void resumeInternal() {
+ checkMainLooperThread();
+ suspended = false;
+ // Undoing what may have been done in suspendInternal().
+ configureMediaFocus(playbackState);
+ }
+
+ public void suspend() {
+ mainHandler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ suspendInternal();
+ }
+ });
+ }
+
+ private void suspendInternal() {
+ checkMainLooperThread();
+ suspended = true;
+
+ // We generally believe the HTML5 app playback state as the source of truth for configuring
+ // media focus since only it can know about a momentary pause between videos in a playlist, or
+ // other autoplay scenario when we should keep media focus. However, when suspending, any
+ // active SbPlayer is destroyed and we release media focus, even if the HTML5 app still thinks
+ // it's in a playing state. We'll configure it again in resumeInternal() and the HTML5 app will
+ // be none the wiser.
+ playbackStateBuilder.setState(
+ playbackState,
+ PlaybackState.PLAYBACK_POSITION_UNKNOWN,
+ playbackState == PLAYBACK_STATE_PLAYING ? 1.0f : 0.0f);
+ configureMediaFocus(PLAYBACK_STATE_NONE);
+ }
+
+ private static void nativeInvokeAction(long action) {
+ nativeInvokeAction(action, 0);
+ }
+
+ private static native void nativeInvokeAction(long action, long seekMs);
+
+ public void updateMediaSession(
+ final int playbackState,
+ final long actions,
+ final long positionMs,
+ final float speed,
+ final String title,
+ final String artist,
+ final String album,
+ final MediaImage[] artwork) {
+ mainHandler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ updateMediaSessionInternal(
+ playbackState, actions, positionMs, speed,
+ title, artist, album, artwork);
+ }
+ });
+ }
+
+ /** Called on main looper thread when media session changes. */
+ private void updateMediaSessionInternal(
+ int playbackState,
+ long actions,
+ long positionMs,
+ float speed,
+ String title,
+ String artist,
+ String album,
+ MediaImage[] artwork) {
+ checkMainLooperThread();
+
+ // Always keep track of what the HTML5 app thinks the playback state is so we can configure the
+ // media focus correctly, either immediately or when resuming from being suspended.
+ this.playbackState = playbackState;
+
+ // Don't update anything while suspended.
+ if (suspended) {
+ Log.i(TAG, "Playback state change while suspended: " + PLAYBACK_STATE_NAME[playbackState]);
+ return;
+ }
+
+ configureMediaFocus(playbackState);
+
+ // Ignore updates to the MediaSession metadata if playback is stopped.
+ if (playbackState == PLAYBACK_STATE_NONE) {
+ return;
+ }
+
+ int androidPlaybackState;
+ String stateName;
+ switch (playbackState) {
+ case PLAYBACK_STATE_PLAYING:
+ androidPlaybackState = PlaybackState.STATE_PLAYING;
+ stateName = "PLAYING";
+ break;
+ case PLAYBACK_STATE_PAUSED:
+ androidPlaybackState = PlaybackState.STATE_PAUSED;
+ stateName = "PAUSED";
+ break;
+ case PLAYBACK_STATE_NONE:
+ default:
+ androidPlaybackState = PlaybackState.STATE_NONE;
+ stateName = "NONE";
+ break;
+ }
+
+ Log.i(TAG, String.format(
+ "MediaSession state: %s, position: %d ms, speed: %f x", stateName, positionMs, speed));
+
+ playbackStateBuilder =
+ new PlaybackState.Builder()
+ .setActions(actions)
+ .setState(androidPlaybackState, positionMs, speed);
+ mediaSession.setPlaybackState(playbackStateBuilder.build());
+
+ // Reset the metadata to make sure we don't retain any fields from previous playback.
+ metadataBuilder = new MediaMetadata.Builder();
+ metadataBuilder
+ .putString(MediaMetadata.METADATA_KEY_TITLE, title)
+ .putString(MediaMetadata.METADATA_KEY_ARTIST, artist)
+ .putString(MediaMetadata.METADATA_KEY_ALBUM, album)
+ .putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, artworkLoader.getOrLoadArtwork(artwork));
+ // Update the metadata as soon as we can - even before artwork is loaded.
+ mediaSession.setMetadata(metadataBuilder.build());
+ }
+
+ @Override
+ public void onArtworkLoaded(Bitmap bitmap) {
+ metadataBuilder.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, bitmap);
+ mediaSession.setMetadata(metadataBuilder.build());
+ }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/Log.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/Log.java
new file mode 100644
index 0000000..44e51b8
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/Log.java
@@ -0,0 +1,24 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.media;
+
+/**
+ * Common definitions for logging in the media package.
+ */
+public class Log {
+ public static final String TAG = "starboard_media";
+
+ private Log() {}
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecBridge.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecBridge.java
new file mode 100644
index 0000000..5fd0a8b
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecBridge.java
@@ -0,0 +1,953 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+// Modifications Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.media;
+
+import static dev.cobalt.media.Log.TAG;
+
+import android.media.AudioFormat;
+import android.media.MediaCodec;
+import android.media.MediaCodec.CryptoInfo;
+import android.media.MediaCodecInfo.VideoCapabilities;
+import android.media.MediaCrypto;
+import android.media.MediaFormat;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.Surface;
+import dev.cobalt.util.Log;
+import dev.cobalt.util.UsedByNative;
+import java.nio.ByteBuffer;
+
+/** A wrapper of the MediaCodec class. */
+@SuppressWarnings("unused")
+@UsedByNative
+class MediaCodecBridge {
+ // Error code for MediaCodecBridge. Keep this value in sync with
+ // MEDIA_CODEC_* values in media_codec_bridge.h.
+ private static final int MEDIA_CODEC_OK = 0;
+ private static final int MEDIA_CODEC_DEQUEUE_INPUT_AGAIN_LATER = 1;
+ private static final int MEDIA_CODEC_DEQUEUE_OUTPUT_AGAIN_LATER = 2;
+ private static final int MEDIA_CODEC_OUTPUT_BUFFERS_CHANGED = 3;
+ private static final int MEDIA_CODEC_OUTPUT_FORMAT_CHANGED = 4;
+ private static final int MEDIA_CODEC_INPUT_END_OF_STREAM = 5;
+ private static final int MEDIA_CODEC_OUTPUT_END_OF_STREAM = 6;
+ private static final int MEDIA_CODEC_NO_KEY = 7;
+ private static final int MEDIA_CODEC_INSUFFICIENT_OUTPUT_PROTECTION = 8;
+ private static final int MEDIA_CODEC_ABORT = 9;
+ private static final int MEDIA_CODEC_ERROR = 10;
+
+ // After a flush(), dequeueOutputBuffer() can often produce empty presentation timestamps
+ // for several frames. As a result, the player may find that the time does not increase
+ // after decoding a frame. To detect this, we check whether the presentation timestamp from
+ // dequeueOutputBuffer() is larger than input_timestamp - MAX_PRESENTATION_TIMESTAMP_SHIFT_US
+ // after a flush. And we set the presentation timestamp from dequeueOutputBuffer() to be
+ // non-decreasing for the remaining frames.
+ private static final long MAX_PRESENTATION_TIMESTAMP_SHIFT_US = 100000;
+
+ // We use only one output audio format (PCM16) that has 2 bytes per sample
+ private static final int PCM16_BYTES_PER_SAMPLE = 2;
+
+ // TODO: Use MediaFormat constants when part of the public API.
+ private static final String KEY_CROP_LEFT = "crop-left";
+ private static final String KEY_CROP_RIGHT = "crop-right";
+ private static final String KEY_CROP_BOTTOM = "crop-bottom";
+ private static final String KEY_CROP_TOP = "crop-top";
+
+ private static final int BITRATE_ADJUSTMENT_FPS = 30;
+ private static final int MAXIMUM_INITIAL_FPS = 30;
+
+ private MediaCodec mMediaCodec;
+ private boolean mFlushed;
+ private long mLastPresentationTimeUs;
+ private final String mMime;
+ private boolean mAdaptivePlaybackSupported;
+
+ // Functions that require this will be called frequently in a tight loop.
+ // Only create one of these and reuse it to avoid excessive allocations,
+ // which would cause GC cycles long enough to impact playback.
+ private final MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
+
+ // Type of bitrate adjustment for video encoder.
+ public enum BitrateAdjustmentTypes {
+ // No adjustment - video encoder has no known bitrate problem.
+ NO_ADJUSTMENT,
+ // Framerate based bitrate adjustment is required - HW encoder does not use frame
+ // timestamps to calculate frame bitrate budget and instead is relying on initial
+ // fps configuration assuming that all frames are coming at fixed initial frame rate.
+ FRAMERATE_ADJUSTMENT,
+ }
+
+ public static final class MimeTypes {
+ public static final String VIDEO_MP4 = "video/mp4";
+ public static final String VIDEO_WEBM = "video/webm";
+ public static final String VIDEO_H264 = "video/avc";
+ public static final String VIDEO_H265 = "video/hevc";
+ public static final String VIDEO_VP8 = "video/x-vnd.on2.vp8";
+ public static final String VIDEO_VP9 = "video/x-vnd.on2.vp9";
+ }
+
+ private BitrateAdjustmentTypes mBitrateAdjustmentType = BitrateAdjustmentTypes.NO_ADJUSTMENT;
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ public static class DequeueInputResult {
+ private int mStatus;
+ private int mIndex;
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private DequeueInputResult() {
+ mStatus = MEDIA_CODEC_ERROR;
+ mIndex = -1;
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private DequeueInputResult(int status, int index) {
+ mStatus = status;
+ mIndex = index;
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private int status() {
+ return mStatus;
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private int index() {
+ return mIndex;
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private static class DequeueOutputResult {
+ private int mStatus;
+ private int mIndex;
+ private int mFlags;
+ private int mOffset;
+ private long mPresentationTimeMicroseconds;
+ private int mNumBytes;
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private DequeueOutputResult() {
+ mStatus = MEDIA_CODEC_ERROR;
+ mIndex = -1;
+ mFlags = 0;
+ mOffset = 0;
+ mPresentationTimeMicroseconds = 0;
+ mNumBytes = 0;
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private DequeueOutputResult(
+ int status,
+ int index,
+ int flags,
+ int offset,
+ long presentationTimeMicroseconds,
+ int numBytes) {
+ mStatus = status;
+ mIndex = index;
+ mFlags = flags;
+ mOffset = offset;
+ mPresentationTimeMicroseconds = presentationTimeMicroseconds;
+ mNumBytes = numBytes;
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private int status() {
+ return mStatus;
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private int index() {
+ return mIndex;
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private int flags() {
+ return mFlags;
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private int offset() {
+ return mOffset;
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private long presentationTimeMicroseconds() {
+ return mPresentationTimeMicroseconds;
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private int numBytes() {
+ return mNumBytes;
+ }
+ }
+
+ /** A wrapper around a MediaFormat. */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private static class GetOutputFormatResult {
+ private int mStatus;
+ // May be null if mStatus is not MEDIA_CODEC_OK.
+ private MediaFormat mFormat;
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private GetOutputFormatResult() {
+ mStatus = MEDIA_CODEC_ERROR;
+ mFormat = null;
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private GetOutputFormatResult(int status, MediaFormat format) {
+ mStatus = status;
+ mFormat = format;
+ }
+
+ private boolean formatHasCropValues() {
+ return mFormat.containsKey(KEY_CROP_RIGHT)
+ && mFormat.containsKey(KEY_CROP_LEFT)
+ && mFormat.containsKey(KEY_CROP_BOTTOM)
+ && mFormat.containsKey(KEY_CROP_TOP);
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private int status() {
+ return mStatus;
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private int width() {
+ return formatHasCropValues()
+ ? mFormat.getInteger(KEY_CROP_RIGHT) - mFormat.getInteger(KEY_CROP_LEFT) + 1
+ : mFormat.getInteger(MediaFormat.KEY_WIDTH);
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private int height() {
+ return formatHasCropValues()
+ ? mFormat.getInteger(KEY_CROP_BOTTOM) - mFormat.getInteger(KEY_CROP_TOP) + 1
+ : mFormat.getInteger(MediaFormat.KEY_HEIGHT);
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private int sampleRate() {
+ return mFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private int channelCount() {
+ return mFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private static class ColorInfo {
+ private static final int MAX_CHROMATICITY = 50000; // Defined in CTA-861.3.
+ private static final int DEFAULT_MAX_CLL = 1000;
+ private static final int DEFAULT_MAX_FALL = 200;
+
+ public int colorRange;
+ public int colorStandard;
+ public int colorTransfer;
+ public ByteBuffer hdrStaticInfo;
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ ColorInfo(
+ int colorRange,
+ int colorStandard,
+ int colorTransfer,
+ float primaryRChromaticityX,
+ float primaryRChromaticityY,
+ float primaryGChromaticityX,
+ float primaryGChromaticityY,
+ float primaryBChromaticityX,
+ float primaryBChromaticityY,
+ float whitePointChromaticityX,
+ float whitePointChromaticityY,
+ float maxMasteringLuminance,
+ float minMasteringLuminance) {
+ this.colorRange = colorRange;
+ this.colorStandard = colorStandard;
+ this.colorTransfer = colorTransfer;
+
+ // This logic is inspired by
+ // https://github.com/google/ExoPlayer/blob/deb9b301b2c7ef66fdd7d8a3e58298a79ba9c619/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java#L1803.
+ byte[] hdrStaticInfoData = new byte[25];
+ ByteBuffer hdrStaticInfo = ByteBuffer.wrap(hdrStaticInfoData);
+ hdrStaticInfo.put((byte) 0);
+ hdrStaticInfo.putShort((short) ((primaryRChromaticityX * MAX_CHROMATICITY) + 0.5f));
+ hdrStaticInfo.putShort((short) ((primaryRChromaticityY * MAX_CHROMATICITY) + 0.5f));
+ hdrStaticInfo.putShort((short) ((primaryGChromaticityX * MAX_CHROMATICITY) + 0.5f));
+ hdrStaticInfo.putShort((short) ((primaryGChromaticityY * MAX_CHROMATICITY) + 0.5f));
+ hdrStaticInfo.putShort((short) ((primaryBChromaticityX * MAX_CHROMATICITY) + 0.5f));
+ hdrStaticInfo.putShort((short) ((primaryBChromaticityY * MAX_CHROMATICITY) + 0.5f));
+ hdrStaticInfo.putShort((short) ((whitePointChromaticityX * MAX_CHROMATICITY) + 0.5f));
+ hdrStaticInfo.putShort((short) ((whitePointChromaticityY * MAX_CHROMATICITY) + 0.5f));
+ hdrStaticInfo.putShort((short) (maxMasteringLuminance + 0.5f));
+ hdrStaticInfo.putShort((short) (minMasteringLuminance + 0.5f));
+ hdrStaticInfo.putShort((short) DEFAULT_MAX_CLL);
+ hdrStaticInfo.putShort((short) DEFAULT_MAX_FALL);
+ this.hdrStaticInfo = hdrStaticInfo;
+ }
+ }
+
+ private MediaCodecBridge(
+ MediaCodec mediaCodec,
+ String mime,
+ boolean adaptivePlaybackSupported,
+ BitrateAdjustmentTypes bitrateAdjustmentType) {
+ if (mediaCodec == null) {
+ throw new IllegalArgumentException();
+ }
+ mMediaCodec = mediaCodec;
+ mMime = mime; // TODO: Delete the unused mMime field
+ mLastPresentationTimeUs = 0;
+ mFlushed = true;
+ mAdaptivePlaybackSupported = adaptivePlaybackSupported;
+ mBitrateAdjustmentType = bitrateAdjustmentType;
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ public static MediaCodecBridge createAudioMediaCodecBridge(
+ String mime,
+ boolean isSecure,
+ boolean requireSoftwareCodec,
+ int sampleRate,
+ int channelCount,
+ MediaCrypto crypto) {
+ MediaCodec mediaCodec = null;
+ try {
+ String decoderName = MediaCodecUtil.findAudioDecoder(mime, 0);
+ if (decoderName.equals("")) {
+ Log.e(TAG, String.format("Failed to find decoder: %s, isSecure: %s", mime, isSecure));
+ return null;
+ }
+ Log.i(TAG, String.format("Creating \"%s\" decoder.", decoderName));
+ mediaCodec = MediaCodec.createByCodecName(decoderName);
+ } catch (Exception e) {
+ Log.e(TAG, String.format("Failed to create MediaCodec: %s, isSecure: %s", mime, isSecure), e);
+ return null;
+ }
+ if (mediaCodec == null) {
+ return null;
+ }
+ MediaCodecBridge bridge =
+ new MediaCodecBridge(mediaCodec, mime, true, BitrateAdjustmentTypes.NO_ADJUSTMENT);
+
+ MediaFormat mediaFormat = createAudioFormat(mime, sampleRate, channelCount);
+ setFrameHasADTSHeader(mediaFormat);
+ if (!bridge.configureAudio(mediaFormat, crypto, 0)) {
+ Log.e(TAG, "Failed to configure audio codec.");
+ bridge.release();
+ return null;
+ }
+ if (!bridge.start()) {
+ Log.e(TAG, "Failed to start audio codec.");
+ bridge.release();
+ return null;
+ }
+
+ return bridge;
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ public static MediaCodecBridge createVideoMediaCodecBridge(
+ String mime,
+ boolean isSecure,
+ boolean requireSoftwareCodec,
+ int width,
+ int height,
+ Surface surface,
+ MediaCrypto crypto,
+ ColorInfo colorInfo) {
+ MediaCodec mediaCodec = null;
+
+ boolean findHDRDecoder = android.os.Build.VERSION.SDK_INT >= 24 && colorInfo != null;
+ // On first pass, try to find a decoder with HDR if the color info is non-null.
+ MediaCodecUtil.FindVideoDecoderResult findVideoDecoderResult =
+ MediaCodecUtil.findVideoDecoder(mime, isSecure, 0, 0, 0, 0, findHDRDecoder);
+ if (findVideoDecoderResult.name.equals("") && findHDRDecoder) {
+ // On second pass, forget HDR.
+ findVideoDecoderResult = MediaCodecUtil.findVideoDecoder(mime, isSecure, 0, 0, 0, 0, false);
+ }
+ try {
+ String decoderName = findVideoDecoderResult.name;
+ if (decoderName.equals("") || findVideoDecoderResult.videoCapabilities == null) {
+ Log.e(TAG, String.format("Failed to find decoder: %s, isSecure: %s", mime, isSecure));
+ return null;
+ }
+ Log.i(TAG, String.format("Creating \"%s\" decoder.", decoderName));
+ mediaCodec = MediaCodec.createByCodecName(decoderName);
+ } catch (Exception e) {
+ Log.e(TAG, String.format("Failed to create MediaCodec: %s, isSecure: %s", mime, isSecure), e);
+ return null;
+ }
+ if (mediaCodec == null) {
+ return null;
+ }
+ MediaCodecBridge bridge =
+ new MediaCodecBridge(mediaCodec, mime, true, BitrateAdjustmentTypes.NO_ADJUSTMENT);
+ MediaFormat mediaFormat =
+ createVideoDecoderFormat(mime, width, height, findVideoDecoderResult.videoCapabilities);
+
+ boolean shouldConfigureHdr =
+ android.os.Build.VERSION.SDK_INT >= 24
+ && colorInfo != null
+ && MediaCodecUtil.isHdrCapableVp9Decoder(findVideoDecoderResult);
+ if (shouldConfigureHdr) {
+ Log.d(TAG, "Setting HDR info.");
+ mediaFormat.setInteger(MediaFormat.KEY_COLOR_TRANSFER, colorInfo.colorTransfer);
+ mediaFormat.setInteger(MediaFormat.KEY_COLOR_STANDARD, colorInfo.colorStandard);
+ mediaFormat.setInteger(MediaFormat.KEY_COLOR_RANGE, colorInfo.colorRange);
+ mediaFormat.setByteBuffer(MediaFormat.KEY_HDR_STATIC_INFO, colorInfo.hdrStaticInfo);
+ }
+
+ int maxWidth = findVideoDecoderResult.videoCapabilities.getSupportedWidths().getUpper();
+ int maxHeight = findVideoDecoderResult.videoCapabilities.getSupportedHeights().getUpper();
+ if (!bridge.configureVideo(mediaFormat, surface, crypto, 0, true, maxWidth, maxHeight)) {
+ Log.e(TAG, "Failed to configure video codec.");
+ bridge.release();
+ return null;
+ }
+ if (!bridge.start()) {
+ Log.e(TAG, "Failed to start video codec.");
+ bridge.release();
+ return null;
+ }
+
+ return bridge;
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ public void release() {
+ try {
+ String codecName = mMediaCodec.getName();
+ Log.w(TAG, "calling MediaCodec.release() on " + codecName);
+ mMediaCodec.release();
+ } catch (IllegalStateException e) {
+ // The MediaCodec is stuck in a wrong state, possibly due to losing
+ // the surface.
+ Log.e(TAG, "Cannot release media codec", e);
+ }
+ mMediaCodec = null;
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private boolean start() {
+ try {
+ mMediaCodec.start();
+ } catch (IllegalStateException | IllegalArgumentException e) {
+ Log.e(TAG, "Cannot start the media codec", e);
+ return false;
+ }
+ return true;
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private void dequeueInputBuffer(long timeoutUs, DequeueInputResult outDequeueInputResult) {
+ int status = MEDIA_CODEC_ERROR;
+ int index = -1;
+ try {
+ int indexOrStatus = mMediaCodec.dequeueInputBuffer(timeoutUs);
+ if (indexOrStatus >= 0) { // index!
+ status = MEDIA_CODEC_OK;
+ index = indexOrStatus;
+ } else if (indexOrStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
+ status = MEDIA_CODEC_DEQUEUE_INPUT_AGAIN_LATER;
+ } else {
+ throw new AssertionError("Unexpected index_or_status: " + indexOrStatus);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to dequeue input buffer", e);
+ }
+ outDequeueInputResult.mStatus = status;
+ outDequeueInputResult.mIndex = index;
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private int flush() {
+ try {
+ mFlushed = true;
+ mMediaCodec.flush();
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Failed to flush MediaCodec", e);
+ return MEDIA_CODEC_ERROR;
+ }
+ return MEDIA_CODEC_OK;
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private void stop() {
+ try {
+ mMediaCodec.stop();
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Failed to stop MediaCodec", e);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private String getName() {
+ String codecName = "unknown";
+ try {
+ codecName = mMediaCodec.getName();
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Cannot get codec name", e);
+ }
+ return codecName;
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private void getOutputFormat(GetOutputFormatResult outGetOutputFormatResult) {
+ MediaFormat format = null;
+ int status = MEDIA_CODEC_OK;
+ try {
+ format = mMediaCodec.getOutputFormat();
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Failed to get output format", e);
+ status = MEDIA_CODEC_ERROR;
+ }
+ outGetOutputFormatResult.mStatus = status;
+ outGetOutputFormatResult.mFormat = format;
+ }
+
+ /** Returns null if MediaCodec throws IllegalStateException. */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private ByteBuffer getInputBuffer(int index) {
+ try {
+ return mMediaCodec.getInputBuffer(index);
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Failed to get input buffer", e);
+ return null;
+ }
+ }
+
+ /** Returns null if MediaCodec throws IllegalStateException. */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private ByteBuffer getOutputBuffer(int index) {
+ try {
+ return mMediaCodec.getOutputBuffer(index);
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Failed to get output buffer", e);
+ return null;
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private int queueInputBuffer(
+ int index, int offset, int size, long presentationTimeUs, int flags) {
+ resetLastPresentationTimeIfNeeded(presentationTimeUs);
+ try {
+ mMediaCodec.queueInputBuffer(index, offset, size, presentationTimeUs, flags);
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to queue input buffer", e);
+ return MEDIA_CODEC_ERROR;
+ }
+ return MEDIA_CODEC_OK;
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private void setVideoBitrate(int bps, int frameRate) {
+ int targetBps = bps;
+ if (mBitrateAdjustmentType == BitrateAdjustmentTypes.FRAMERATE_ADJUSTMENT && frameRate > 0) {
+ targetBps = BITRATE_ADJUSTMENT_FPS * bps / frameRate;
+ }
+
+ Bundle b = new Bundle();
+ b.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, targetBps);
+ try {
+ mMediaCodec.setParameters(b);
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Failed to set MediaCodec parameters", e);
+ }
+ Log.v(TAG, "setVideoBitrate: input " + bps + "bps@" + frameRate + ", targetBps " + targetBps);
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private void requestKeyFrameSoon() {
+ Bundle b = new Bundle();
+ b.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0);
+ try {
+ mMediaCodec.setParameters(b);
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Failed to set MediaCodec parameters", e);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private int queueSecureInputBuffer(
+ int index,
+ int offset,
+ byte[] iv,
+ byte[] keyId,
+ int[] numBytesOfClearData,
+ int[] numBytesOfEncryptedData,
+ int numSubSamples,
+ int cipherMode,
+ int patternEncrypt,
+ int patternSkip,
+ long presentationTimeUs) {
+ resetLastPresentationTimeIfNeeded(presentationTimeUs);
+ try {
+ boolean usesCbcs =
+ Build.VERSION.SDK_INT >= 24 && cipherMode == MediaCodec.CRYPTO_MODE_AES_CBC;
+
+ if (usesCbcs) {
+ Log.e(TAG, "Encryption scheme 'cbcs' not supported on this platform.");
+ return MEDIA_CODEC_ERROR;
+ }
+ CryptoInfo cryptoInfo = new CryptoInfo();
+ cryptoInfo.set(
+ numSubSamples, numBytesOfClearData, numBytesOfEncryptedData, keyId, iv, cipherMode);
+ if (patternEncrypt != 0 && patternSkip != 0) {
+ if (usesCbcs) {
+ // Above platform check ensured that setting the pattern is indeed supported.
+ // MediaCodecUtil.setPatternIfSupported(cryptoInfo, patternEncrypt, patternSkip);
+ Log.e(TAG, "Only AES_CTR is supported.");
+ } else {
+ Log.e(TAG, "Pattern encryption only supported for 'cbcs' scheme (CBC mode).");
+ return MEDIA_CODEC_ERROR;
+ }
+ }
+ mMediaCodec.queueSecureInputBuffer(index, offset, cryptoInfo, presentationTimeUs, 0);
+ } catch (MediaCodec.CryptoException e) {
+ int errorCode = e.getErrorCode();
+ if (errorCode == MediaCodec.CryptoException.ERROR_NO_KEY) {
+ Log.d(TAG, "Failed to queue secure input buffer: CryptoException.ERROR_NO_KEY");
+ return MEDIA_CODEC_NO_KEY;
+ } else if (errorCode == MediaCodec.CryptoException.ERROR_INSUFFICIENT_OUTPUT_PROTECTION) {
+ Log.d(
+ TAG,
+ "Failed to queue secure input buffer: "
+ + "CryptoException.ERROR_INSUFFICIENT_OUTPUT_PROTECTION");
+ // Note that in Android OS version before 23, the MediaDrm class doesn't expose the current
+ // key ids it holds. In such case the Starboard media stack is unable to notify Cobalt of
+ // the error via key statuses so MEDIA_CODEC_ERROR is returned instead to signal a general
+ // media codec error.
+ return Build.VERSION.SDK_INT >= 23
+ ? MEDIA_CODEC_INSUFFICIENT_OUTPUT_PROTECTION
+ : MEDIA_CODEC_ERROR;
+ }
+ Log.e(
+ TAG,
+ "Failed to queue secure input buffer, CryptoException with error code "
+ + e.getErrorCode());
+ return MEDIA_CODEC_ERROR;
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Failed to queue secure input buffer, IllegalStateException " + e);
+ return MEDIA_CODEC_ERROR;
+ }
+ return MEDIA_CODEC_OK;
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private void releaseOutputBuffer(int index, boolean render) {
+ try {
+ mMediaCodec.releaseOutputBuffer(index, render);
+ } catch (IllegalStateException e) {
+ // TODO: May need to report the error to the caller. crbug.com/356498.
+ Log.e(TAG, "Failed to release output buffer", e);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private void releaseOutputBuffer(int index, long renderTimestampNs) {
+ try {
+ mMediaCodec.releaseOutputBuffer(index, renderTimestampNs);
+ } catch (IllegalStateException e) {
+ // TODO: May need to report the error to the caller. crbug.com/356498.
+ Log.e(TAG, "Failed to release output buffer", e);
+ }
+ }
+
+ @SuppressWarnings({"unused", "deprecation"})
+ @UsedByNative
+ private void dequeueOutputBuffer(long timeoutUs, DequeueOutputResult outDequeueOutputResult) {
+ int status = MEDIA_CODEC_ERROR;
+ int index = -1;
+ try {
+ int indexOrStatus = mMediaCodec.dequeueOutputBuffer(info, timeoutUs);
+ if (info.presentationTimeUs < mLastPresentationTimeUs) {
+ // TODO: return a special code through DequeueOutputResult
+ // to notify the native code that the frame has a wrong presentation
+ // timestamp and should be skipped.
+ info.presentationTimeUs = mLastPresentationTimeUs;
+ }
+ mLastPresentationTimeUs = info.presentationTimeUs;
+
+ if (indexOrStatus >= 0) { // index!
+ status = MEDIA_CODEC_OK;
+ index = indexOrStatus;
+ } else if (indexOrStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
+ status = MEDIA_CODEC_OUTPUT_BUFFERS_CHANGED;
+ } else if (indexOrStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
+ status = MEDIA_CODEC_OUTPUT_FORMAT_CHANGED;
+ MediaFormat newFormat = mMediaCodec.getOutputFormat();
+ } else if (indexOrStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
+ status = MEDIA_CODEC_DEQUEUE_OUTPUT_AGAIN_LATER;
+ } else {
+ throw new AssertionError("Unexpected index_or_status: " + indexOrStatus);
+ }
+ } catch (IllegalStateException e) {
+ status = MEDIA_CODEC_ERROR;
+ Log.e(TAG, "Failed to dequeue output buffer", e);
+ }
+
+ outDequeueOutputResult.mStatus = status;
+ outDequeueOutputResult.mIndex = index;
+ outDequeueOutputResult.mFlags = info.flags;
+ outDequeueOutputResult.mOffset = info.offset;
+ outDequeueOutputResult.mPresentationTimeMicroseconds = info.presentationTimeUs;
+ outDequeueOutputResult.mNumBytes = info.size;
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private boolean configureVideo(
+ MediaFormat format,
+ Surface surface,
+ MediaCrypto crypto,
+ int flags,
+ boolean allowAdaptivePlayback,
+ int maxSupportedWidth,
+ int maxSupportedHeight) {
+ try {
+ // If adaptive playback is turned off by request, then treat it as
+ // not supported. Note that configureVideo is only called once
+ // during creation, else this would prevent re-enabling adaptive
+ // playback later.
+ if (!allowAdaptivePlayback) {
+ mAdaptivePlaybackSupported = false;
+ }
+
+ if (mAdaptivePlaybackSupported) {
+ // Since we haven't passed the properties of the stream we're playing
+ // down to this level, from our perspective, we could potentially
+ // adapt up to 4k at any point. We thus request 4k buffers up front,
+ // unless the decoder claims to not be able to do 4k, in which case
+ // we're ok, since we would've rejected a 4k stream when canPlayType
+ // was called, and then use those decoder values instead.
+ int maxWidth = Math.min(3840, maxSupportedWidth);
+ int maxHeight = Math.min(2160, maxSupportedHeight);
+ format.setInteger(MediaFormat.KEY_MAX_WIDTH, maxWidth);
+ format.setInteger(MediaFormat.KEY_MAX_HEIGHT, maxHeight);
+ }
+ maybeSetMaxInputSize(format);
+ mMediaCodec.configure(format, surface, crypto, flags);
+ return true;
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Cannot configure the video codec, wrong format or surface", e);
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Cannot configure the video codec", e);
+ } catch (MediaCodec.CryptoException e) {
+ Log.e(TAG, "Cannot configure the video codec: DRM error", e);
+ } catch (Exception e) {
+ Log.e(TAG, "Cannot configure the video codec", e);
+ }
+ return false;
+ }
+
+ public static MediaFormat createAudioFormat(String mime, int sampleRate, int channelCount) {
+ return MediaFormat.createAudioFormat(mime, sampleRate, channelCount);
+ }
+
+ private static MediaFormat createVideoDecoderFormat(
+ String mime, int width, int height, VideoCapabilities videoCapabilities) {
+ return MediaFormat.createVideoFormat(
+ mime,
+ alignDimension(width, videoCapabilities.getWidthAlignment()),
+ alignDimension(height, videoCapabilities.getHeightAlignment()));
+ }
+
+ private static int alignDimension(int size, int alignment) {
+ int ceilDivide = (size + alignment - 1) / alignment;
+ return ceilDivide * alignment;
+ }
+
+ // Use some heuristics to set KEY_MAX_INPUT_SIZE (the size of the input buffers).
+ // Taken from exoplayer:
+ // https://github.com/google/ExoPlayer/blob/8595c65678a181296cdf673eacb93d8135479340/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java
+ private void maybeSetMaxInputSize(MediaFormat format) {
+ if (format.containsKey(android.media.MediaFormat.KEY_MAX_INPUT_SIZE)) {
+ // Already set. The source of the format may know better, so do nothing.
+ return;
+ }
+ int maxHeight = format.getInteger(MediaFormat.KEY_HEIGHT);
+ if (mAdaptivePlaybackSupported && format.containsKey(MediaFormat.KEY_MAX_HEIGHT)) {
+ maxHeight = Math.max(maxHeight, format.getInteger(MediaFormat.KEY_MAX_HEIGHT));
+ }
+ int maxWidth = format.getInteger(MediaFormat.KEY_WIDTH);
+ if (mAdaptivePlaybackSupported && format.containsKey(MediaFormat.KEY_MAX_WIDTH)) {
+ maxWidth = Math.max(maxHeight, format.getInteger(MediaFormat.KEY_MAX_WIDTH));
+ }
+ int maxPixels;
+ int minCompressionRatio;
+ switch (format.getString(MediaFormat.KEY_MIME)) {
+ case MimeTypes.VIDEO_H264:
+ if ("BRAVIA 4K 2015".equals(Build.MODEL)) {
+ // The Sony BRAVIA 4k TV has input buffers that are too small for the calculated
+ // 4k video maximum input size, so use the default value.
+ return;
+ }
+ // Round up width/height to an integer number of macroblocks.
+ maxPixels = ((maxWidth + 15) / 16) * ((maxHeight + 15) / 16) * 16 * 16;
+ minCompressionRatio = 2;
+ break;
+ case MimeTypes.VIDEO_VP8:
+ // VPX does not specify a ratio so use the values from the platform's SoftVPX.cpp.
+ maxPixels = maxWidth * maxHeight;
+ minCompressionRatio = 2;
+ break;
+ case MimeTypes.VIDEO_H265:
+ case MimeTypes.VIDEO_VP9:
+ maxPixels = maxWidth * maxHeight;
+ minCompressionRatio = 4;
+ break;
+ default:
+ // Leave the default max input size.
+ return;
+ }
+ // Estimate the maximum input size assuming three channel 4:2:0 subsampled input frames.
+ int maxInputSize = (maxPixels * 3) / (2 * minCompressionRatio);
+ format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize);
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private boolean isAdaptivePlaybackSupported(int width, int height) {
+ // If media codec has adaptive playback supported, then the max sizes
+ // used during creation are only hints.
+ return mAdaptivePlaybackSupported;
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private static void setCodecSpecificData(MediaFormat format, int index, byte[] bytes) {
+ // Codec Specific Data is set in the MediaFormat as ByteBuffer entries with keys csd-0,
+ // csd-1, and so on. See: http://developer.android.com/reference/android/media/MediaCodec.html
+ // for details.
+ String name;
+ switch (index) {
+ case 0:
+ name = "csd-0";
+ break;
+ case 1:
+ name = "csd-1";
+ break;
+ case 2:
+ name = "csd-2";
+ break;
+ default:
+ name = null;
+ break;
+ }
+ if (name != null) {
+ format.setByteBuffer(name, ByteBuffer.wrap(bytes));
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private static void setFrameHasADTSHeader(MediaFormat format) {
+ format.setInteger(MediaFormat.KEY_IS_ADTS, 1);
+ }
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ private boolean configureAudio(MediaFormat format, MediaCrypto crypto, int flags) {
+ try {
+ mMediaCodec.configure(format, null, crypto, flags);
+ return true;
+ } catch (IllegalArgumentException | IllegalStateException e) {
+ Log.e(TAG, "Cannot configure the audio codec", e);
+ } catch (MediaCodec.CryptoException e) {
+ Log.e(TAG, "Cannot configure the audio codec: DRM error", e);
+ } catch (Exception e) {
+ Log.e(TAG, "Cannot configure the audio codec", e);
+ }
+ return false;
+ }
+
+ private void resetLastPresentationTimeIfNeeded(long presentationTimeUs) {
+ if (mFlushed) {
+ mLastPresentationTimeUs =
+ Math.max(presentationTimeUs - MAX_PRESENTATION_TIMESTAMP_SHIFT_US, 0);
+ mFlushed = false;
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ private int getAudioFormat(int channelCount) {
+ switch (channelCount) {
+ case 1:
+ return AudioFormat.CHANNEL_OUT_MONO;
+ case 2:
+ return AudioFormat.CHANNEL_OUT_STEREO;
+ case 4:
+ return AudioFormat.CHANNEL_OUT_QUAD;
+ case 6:
+ return AudioFormat.CHANNEL_OUT_5POINT1;
+ case 8:
+ if (Build.VERSION.SDK_INT >= 23) {
+ return AudioFormat.CHANNEL_OUT_7POINT1_SURROUND;
+ } else {
+ return AudioFormat.CHANNEL_OUT_7POINT1;
+ }
+ default:
+ return AudioFormat.CHANNEL_OUT_DEFAULT;
+ }
+ }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecUtil.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecUtil.java
new file mode 100644
index 0000000..b7fdad8
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecUtil.java
@@ -0,0 +1,660 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.media;
+
+import static dev.cobalt.media.Log.TAG;
+
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecInfo.AudioCapabilities;
+import android.media.MediaCodecInfo.CodecCapabilities;
+import android.media.MediaCodecInfo.CodecProfileLevel;
+import android.media.MediaCodecInfo.VideoCapabilities;
+import android.media.MediaCodecList;
+import android.os.Build;
+import dev.cobalt.util.IsEmulator;
+import dev.cobalt.util.Log;
+import dev.cobalt.util.UsedByNative;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/** Utility functions for dealing with MediaCodec related things. */
+public class MediaCodecUtil {
+ // A low priority black list of codec names that should never be used.
+ private static final Set<String> codecBlackList = new HashSet<>();
+ // A high priority white list of brands/model that should always attempt to
+ // play vp9.
+ private static final Map<String, Set<String>> vp9WhiteList = new HashMap<>();
+ // Whether we should report vp9 codecs as supported or not. Will be set
+ // based on whether vp9WhiteList contains our brand/model. If this is set
+ // to true, then codecBlackList will be ignored.
+ private static boolean isVp9WhiteListed;
+ private static final String SECURE_DECODER_SUFFIX = ".secure";
+ private static final String VP9_MIME_TYPE = "video/x-vnd.on2.vp9";
+
+ /**
+ * A simple "struct" to bundle up the results from findVideoDecoder, as its clients may require
+ * the max supported width and height in addition to just the decoder name.
+ */
+ public static final class FindVideoDecoderResult {
+ public String name;
+ public VideoCapabilities videoCapabilities;
+ public CodecCapabilities codecCapabilities;
+
+ public FindVideoDecoderResult(
+ String name, VideoCapabilities videoCapabilities, CodecCapabilities codecCapabilities) {
+ this.name = name;
+ this.videoCapabilities = videoCapabilities;
+ this.codecCapabilities = codecCapabilities;
+ }
+ }
+
+ static {
+ if (Build.VERSION.SDK_INT >= 24 && Build.BRAND.equals("google")) {
+ codecBlackList.add("OMX.Nvidia.vp9.decode");
+ }
+ if (Build.VERSION.SDK_INT >= 24 && Build.BRAND.equals("LGE")) {
+ codecBlackList.add("OMX.qcom.video.decoder.vp9");
+ }
+ if (Build.VERSION.RELEASE.startsWith("6.0.1")) {
+ codecBlackList.add("OMX.Exynos.vp9.dec");
+ codecBlackList.add("OMX.Intel.VideoDecoder.VP9.hwr");
+ codecBlackList.add("OMX.MTK.VIDEO.DECODER.VP9");
+ codecBlackList.add("OMX.qcom.video.decoder.vp9");
+ }
+ if (Build.VERSION.RELEASE.startsWith("6.0")) {
+ codecBlackList.add("OMX.MTK.VIDEO.DECODER.VP9");
+ codecBlackList.add("OMX.Nvidia.vp9.decode");
+ }
+ if (Build.VERSION.RELEASE.startsWith("5.1.1")) {
+ codecBlackList.add("OMX.allwinner.video.decoder.vp9");
+ codecBlackList.add("OMX.Exynos.vp9.dec");
+ codecBlackList.add("OMX.Intel.VideoDecoder.VP9.hwr");
+ codecBlackList.add("OMX.MTK.VIDEO.DECODER.VP9");
+ codecBlackList.add("OMX.qcom.video.decoder.vp9");
+ }
+ if (Build.VERSION.RELEASE.startsWith("5.1")) {
+ codecBlackList.add("OMX.Exynos.VP9.Decoder");
+ codecBlackList.add("OMX.Intel.VideoDecoder.VP9.hwr");
+ codecBlackList.add("OMX.MTK.VIDEO.DECODER.VP9");
+ }
+ if (Build.VERSION.RELEASE.startsWith("5.0")) {
+ codecBlackList.add("OMX.allwinner.video.decoder.vp9");
+ codecBlackList.add("OMX.Exynos.vp9.dec");
+ codecBlackList.add("OMX.Intel.VideoDecoder.VP9.hwr");
+ codecBlackList.add("OMX.MTK.VIDEO.DECODER.VP9");
+ }
+
+ if (Build.BRAND.equals("google")) {
+ codecBlackList.add("OMX.Intel.VideoDecoder.VP9.hybrid");
+ }
+
+ // Black list non hardware media codec names if we aren't running on an emulator.
+ if (!IsEmulator.isEmulator()) {
+ codecBlackList.add("OMX.ffmpeg.vp9.decoder");
+ codecBlackList.add("OMX.Intel.sw_vd.vp9");
+ codecBlackList.add("OMX.MTK.VIDEO.DECODER.SW.VP9");
+ }
+
+ // Black list the Google software vp9 decoder both on hardware and on the emulator.
+ // On the emulator it fails with the log: "storeMetaDataInBuffers failed w/ err -1010"
+ codecBlackList.add("OMX.google.vp9.decoder");
+
+ vp9WhiteList.put("Amlogic", new HashSet<String>());
+ vp9WhiteList.put("Arcadyan", new HashSet<String>());
+ vp9WhiteList.put("arcelik", new HashSet<String>());
+ vp9WhiteList.put("BNO", new HashSet<String>());
+ vp9WhiteList.put("BROADCOM", new HashSet<String>());
+ vp9WhiteList.put("broadcom", new HashSet<String>());
+ vp9WhiteList.put("Foxconn", new HashSet<String>());
+ vp9WhiteList.put("Freebox", new HashSet<String>());
+ vp9WhiteList.put("Funai", new HashSet<String>());
+ vp9WhiteList.put("gfiber", new HashSet<String>());
+ vp9WhiteList.put("Google", new HashSet<String>());
+ vp9WhiteList.put("google", new HashSet<String>());
+ vp9WhiteList.put("Hisense", new HashSet<String>());
+ vp9WhiteList.put("HUAWEI", new HashSet<String>());
+ vp9WhiteList.put("KaonMedia", new HashSet<String>());
+ vp9WhiteList.put("LeTV", new HashSet<String>());
+ vp9WhiteList.put("LGE", new HashSet<String>());
+ vp9WhiteList.put("MediaTek", new HashSet<String>());
+ vp9WhiteList.put("MStar", new HashSet<String>());
+ vp9WhiteList.put("MTK", new HashSet<String>());
+ vp9WhiteList.put("NVIDIA", new HashSet<String>());
+ vp9WhiteList.put("PHILIPS", new HashSet<String>());
+ vp9WhiteList.put("Philips", new HashSet<String>());
+ vp9WhiteList.put("PIXELA CORPORATION", new HashSet<String>());
+ vp9WhiteList.put("RCA", new HashSet<String>());
+ vp9WhiteList.put("Sagemcom", new HashSet<String>());
+ vp9WhiteList.put("samsung", new HashSet<String>());
+ vp9WhiteList.put("SHARP", new HashSet<String>());
+ vp9WhiteList.put("Skyworth", new HashSet<String>());
+ vp9WhiteList.put("Sony", new HashSet<String>());
+ vp9WhiteList.put("STMicroelectronics", new HashSet<String>());
+ vp9WhiteList.put("SumitomoElectricIndustries", new HashSet<String>());
+ vp9WhiteList.put("TCL", new HashSet<String>());
+ vp9WhiteList.put("Technicolor", new HashSet<String>());
+ vp9WhiteList.put("Vestel", new HashSet<String>());
+ vp9WhiteList.put("wnc", new HashSet<String>());
+ vp9WhiteList.put("Xiaomi", new HashSet<String>());
+ vp9WhiteList.put("ZTE TV", new HashSet<String>());
+
+ vp9WhiteList.get("Amlogic").add("p212");
+ vp9WhiteList.get("Arcadyan").add("Bouygtel4K");
+ vp9WhiteList.get("Arcadyan").add("HMB2213PW22TS");
+ vp9WhiteList.get("Arcadyan").add("IPSetTopBox");
+ vp9WhiteList.get("arcelik").add("arcelik_uhd_powermax_at");
+ vp9WhiteList.get("BNO").add("QM153E");
+ vp9WhiteList.get("broadcom").add("avko");
+ vp9WhiteList.get("broadcom").add("banff");
+ vp9WhiteList.get("BROADCOM").add("BCM7XXX_TEST_SETTOP");
+ vp9WhiteList.get("broadcom").add("cypress");
+ vp9WhiteList.get("broadcom").add("dawson");
+ vp9WhiteList.get("broadcom").add("elfin");
+ vp9WhiteList.get("Foxconn").add("ba101");
+ vp9WhiteList.get("Foxconn").add("bd201");
+ vp9WhiteList.get("Freebox").add("Freebox Player Mini v2");
+ vp9WhiteList.get("Funai").add("PHILIPS 4K TV");
+ vp9WhiteList.get("gfiber").add("GFHD254");
+ vp9WhiteList.get("google").add("avko");
+ vp9WhiteList.get("google").add("marlin");
+ vp9WhiteList.get("Google").add("Pixel XL");
+ vp9WhiteList.get("Google").add("Pixel");
+ vp9WhiteList.get("google").add("sailfish");
+ vp9WhiteList.get("google").add("sprint");
+ vp9WhiteList.get("Hisense").add("HAT4KDTV");
+ vp9WhiteList.get("HUAWEI").add("X21");
+ vp9WhiteList.get("KaonMedia").add("IC1110");
+ vp9WhiteList.get("KaonMedia").add("IC1130");
+ vp9WhiteList.get("KaonMedia").add("MCM4000");
+ vp9WhiteList.get("KaonMedia").add("PRDMK100T");
+ vp9WhiteList.get("KaonMedia").add("SFCSTB2LITE");
+ vp9WhiteList.get("LeTV").add("uMax85");
+ vp9WhiteList.get("LeTV").add("X4-43Pro");
+ vp9WhiteList.get("LeTV").add("X4-55");
+ vp9WhiteList.get("LeTV").add("X4-65");
+ vp9WhiteList.get("LGE").add("S60CLI");
+ vp9WhiteList.get("LGE").add("S60UPA");
+ vp9WhiteList.get("LGE").add("S60UPI");
+ vp9WhiteList.get("LGE").add("S70CDS");
+ vp9WhiteList.get("LGE").add("S70PCI");
+ vp9WhiteList.get("LGE").add("SH960C-DS");
+ vp9WhiteList.get("LGE").add("SH960C-LN");
+ vp9WhiteList.get("LGE").add("SH960S-AT");
+ vp9WhiteList.get("MediaTek").add("Archer");
+ vp9WhiteList.get("MediaTek").add("augie");
+ vp9WhiteList.get("MediaTek").add("kane");
+ vp9WhiteList.get("MStar").add("Denali");
+ vp9WhiteList.get("MStar").add("Rainier");
+ vp9WhiteList.get("MTK").add("Generic Android on sharp_2k15_us_android");
+ vp9WhiteList.get("NVIDIA").add("SHIELD Android TV");
+ vp9WhiteList.get("NVIDIA").add("SHIELD Console");
+ vp9WhiteList.get("NVIDIA").add("SHIELD Portable");
+ vp9WhiteList.get("PHILIPS").add("QM151E");
+ vp9WhiteList.get("PHILIPS").add("QM161E");
+ vp9WhiteList.get("PHILIPS").add("QM163E");
+ vp9WhiteList.get("Philips").add("TPM171E");
+ vp9WhiteList.get("PIXELA CORPORATION").add("POE-MP4000");
+ vp9WhiteList.get("RCA").add("XLDRCAV1");
+ vp9WhiteList.get("Sagemcom").add("DNA Android TV");
+ vp9WhiteList.get("Sagemcom").add("GigaTV");
+ vp9WhiteList.get("Sagemcom").add("M387_QL");
+ vp9WhiteList.get("Sagemcom").add("Sagemcom Android STB");
+ vp9WhiteList.get("Sagemcom").add("Sagemcom ATV Demo");
+ vp9WhiteList.get("Sagemcom").add("Telecable ATV");
+ vp9WhiteList.get("samsung").add("c71kw200");
+ vp9WhiteList.get("samsung").add("GX-CJ680CL");
+ vp9WhiteList.get("samsung").add("SAMSUNG-SM-G890A");
+ vp9WhiteList.get("samsung").add("SAMSUNG-SM-G920A");
+ vp9WhiteList.get("samsung").add("SAMSUNG-SM-G920AZ");
+ vp9WhiteList.get("samsung").add("SAMSUNG-SM-G925A");
+ vp9WhiteList.get("samsung").add("SAMSUNG-SM-G928A");
+ vp9WhiteList.get("samsung").add("SM-G9200");
+ vp9WhiteList.get("samsung").add("SM-G9208");
+ vp9WhiteList.get("samsung").add("SM-G9209");
+ vp9WhiteList.get("samsung").add("SM-G920A");
+ vp9WhiteList.get("samsung").add("SM-G920D");
+ vp9WhiteList.get("samsung").add("SM-G920F");
+ vp9WhiteList.get("samsung").add("SM-G920FD");
+ vp9WhiteList.get("samsung").add("SM-G920FQ");
+ vp9WhiteList.get("samsung").add("SM-G920I");
+ vp9WhiteList.get("samsung").add("SM-G920K");
+ vp9WhiteList.get("samsung").add("SM-G920L");
+ vp9WhiteList.get("samsung").add("SM-G920P");
+ vp9WhiteList.get("samsung").add("SM-G920R4");
+ vp9WhiteList.get("samsung").add("SM-G920R6");
+ vp9WhiteList.get("samsung").add("SM-G920R7");
+ vp9WhiteList.get("samsung").add("SM-G920S");
+ vp9WhiteList.get("samsung").add("SM-G920T");
+ vp9WhiteList.get("samsung").add("SM-G920T1");
+ vp9WhiteList.get("samsung").add("SM-G920V");
+ vp9WhiteList.get("samsung").add("SM-G920W8");
+ vp9WhiteList.get("samsung").add("SM-G9250");
+ vp9WhiteList.get("samsung").add("SM-G925A");
+ vp9WhiteList.get("samsung").add("SM-G925D");
+ vp9WhiteList.get("samsung").add("SM-G925F");
+ vp9WhiteList.get("samsung").add("SM-G925FQ");
+ vp9WhiteList.get("samsung").add("SM-G925I");
+ vp9WhiteList.get("samsung").add("SM-G925J");
+ vp9WhiteList.get("samsung").add("SM-G925K");
+ vp9WhiteList.get("samsung").add("SM-G925L");
+ vp9WhiteList.get("samsung").add("SM-G925P");
+ vp9WhiteList.get("samsung").add("SM-G925R4");
+ vp9WhiteList.get("samsung").add("SM-G925R6");
+ vp9WhiteList.get("samsung").add("SM-G925R7");
+ vp9WhiteList.get("samsung").add("SM-G925S");
+ vp9WhiteList.get("samsung").add("SM-G925T");
+ vp9WhiteList.get("samsung").add("SM-G925V");
+ vp9WhiteList.get("samsung").add("SM-G925W8");
+ vp9WhiteList.get("samsung").add("SM-G925Z");
+ vp9WhiteList.get("samsung").add("SM-G9280");
+ vp9WhiteList.get("samsung").add("SM-G9287");
+ vp9WhiteList.get("samsung").add("SM-G9287C");
+ vp9WhiteList.get("samsung").add("SM-G928A");
+ vp9WhiteList.get("samsung").add("SM-G928C");
+ vp9WhiteList.get("samsung").add("SM-G928F");
+ vp9WhiteList.get("samsung").add("SM-G928G");
+ vp9WhiteList.get("samsung").add("SM-G928I");
+ vp9WhiteList.get("samsung").add("SM-G928K");
+ vp9WhiteList.get("samsung").add("SM-G928L");
+ vp9WhiteList.get("samsung").add("SM-G928N0");
+ vp9WhiteList.get("samsung").add("SM-G928P");
+ vp9WhiteList.get("samsung").add("SM-G928S");
+ vp9WhiteList.get("samsung").add("SM-G928T");
+ vp9WhiteList.get("samsung").add("SM-G928V");
+ vp9WhiteList.get("samsung").add("SM-G928W8");
+ vp9WhiteList.get("samsung").add("SM-G928X");
+ vp9WhiteList.get("samsung").add("SM-G9300");
+ vp9WhiteList.get("samsung").add("SM-G9308");
+ vp9WhiteList.get("samsung").add("SM-G930A");
+ vp9WhiteList.get("samsung").add("SM-G930AZ");
+ vp9WhiteList.get("samsung").add("SM-G930F");
+ vp9WhiteList.get("samsung").add("SM-G930FD");
+ vp9WhiteList.get("samsung").add("SM-G930K");
+ vp9WhiteList.get("samsung").add("SM-G930L");
+ vp9WhiteList.get("samsung").add("SM-G930P");
+ vp9WhiteList.get("samsung").add("SM-G930R4");
+ vp9WhiteList.get("samsung").add("SM-G930R6");
+ vp9WhiteList.get("samsung").add("SM-G930R7");
+ vp9WhiteList.get("samsung").add("SM-G930S");
+ vp9WhiteList.get("samsung").add("SM-G930T");
+ vp9WhiteList.get("samsung").add("SM-G930T1");
+ vp9WhiteList.get("samsung").add("SM-G930U");
+ vp9WhiteList.get("samsung").add("SM-G930V");
+ vp9WhiteList.get("samsung").add("SM-G930VL");
+ vp9WhiteList.get("samsung").add("SM-G930W8");
+ vp9WhiteList.get("samsung").add("SM-G9350");
+ vp9WhiteList.get("samsung").add("SM-G935A");
+ vp9WhiteList.get("samsung").add("SM-G935D");
+ vp9WhiteList.get("samsung").add("SM-G935F");
+ vp9WhiteList.get("samsung").add("SM-G935FD");
+ vp9WhiteList.get("samsung").add("SM-G935J");
+ vp9WhiteList.get("samsung").add("SM-G935K");
+ vp9WhiteList.get("samsung").add("SM-G935L");
+ vp9WhiteList.get("samsung").add("SM-G935P");
+ vp9WhiteList.get("samsung").add("SM-G935R4");
+ vp9WhiteList.get("samsung").add("SM-G935S");
+ vp9WhiteList.get("samsung").add("SM-G935T");
+ vp9WhiteList.get("samsung").add("SM-G935U");
+ vp9WhiteList.get("samsung").add("SM-G935V");
+ vp9WhiteList.get("samsung").add("SM-G935W8");
+ vp9WhiteList.get("samsung").add("SM-N9200");
+ vp9WhiteList.get("samsung").add("SM-N9208");
+ vp9WhiteList.get("samsung").add("SM-N920A");
+ vp9WhiteList.get("samsung").add("SM-N920C");
+ vp9WhiteList.get("samsung").add("SM-N920F");
+ vp9WhiteList.get("samsung").add("SM-N920G");
+ vp9WhiteList.get("samsung").add("SM-N920I");
+ vp9WhiteList.get("samsung").add("SM-N920K");
+ vp9WhiteList.get("samsung").add("SM-N920L");
+ vp9WhiteList.get("samsung").add("SM-N920R4");
+ vp9WhiteList.get("samsung").add("SM-N920R6");
+ vp9WhiteList.get("samsung").add("SM-N920R7");
+ vp9WhiteList.get("samsung").add("SM-N920S");
+ vp9WhiteList.get("samsung").add("SM-N920T");
+ vp9WhiteList.get("samsung").add("SM-N920TP");
+ vp9WhiteList.get("samsung").add("SM-N920V");
+ vp9WhiteList.get("samsung").add("SM-N920W8");
+ vp9WhiteList.get("samsung").add("SM-N920X");
+ vp9WhiteList.get("SHARP").add("AN-NP40");
+ vp9WhiteList.get("SHARP").add("AQUOS-4KTVJ17");
+ vp9WhiteList.get("SHARP").add("AQUOS-4KTVT17");
+ vp9WhiteList.get("SHARP").add("AQUOS-4KTVX17");
+ vp9WhiteList.get("SHARP").add("LC-U35T");
+ vp9WhiteList.get("SHARP").add("LC-UE630X");
+ vp9WhiteList.get("SHARP").add("LC-Ux30US");
+ vp9WhiteList.get("SHARP").add("LC-XU35T");
+ vp9WhiteList.get("SHARP").add("LC-XU930X_830X");
+ vp9WhiteList.get("Skyworth").add("globe");
+ vp9WhiteList.get("Sony").add("Amai VP9");
+ vp9WhiteList.get("Sony").add("BRAVIA 4K 2015");
+ vp9WhiteList.get("Sony").add("BRAVIA 4K GB");
+ vp9WhiteList.get("STMicroelectronics").add("sti4k");
+ vp9WhiteList.get("SumitomoElectricIndustries").add("C02AS");
+ vp9WhiteList.get("SumitomoElectricIndustries").add("ST4173");
+ vp9WhiteList.get("SumitomoElectricIndustries").add("test_STW2000");
+ vp9WhiteList.get("TCL").add("Percee TV");
+ vp9WhiteList.get("Technicolor").add("AirTV Player");
+ vp9WhiteList.get("Technicolor").add("Bouygtel4K");
+ vp9WhiteList.get("Technicolor").add("CM-7600");
+ vp9WhiteList.get("Technicolor").add("cooper");
+ vp9WhiteList.get("Technicolor").add("Foxtel Now box");
+ vp9WhiteList.get("Technicolor").add("pearl");
+ vp9WhiteList.get("Technicolor").add("Sapphire");
+ vp9WhiteList.get("Technicolor").add("Shortcut");
+ vp9WhiteList.get("Technicolor").add("skipper");
+ vp9WhiteList.get("Technicolor").add("STING");
+ vp9WhiteList.get("Technicolor").add("TIM_BOX");
+ vp9WhiteList.get("Technicolor").add("uzx8020chm");
+ vp9WhiteList.get("Vestel").add("S7252");
+ vp9WhiteList.get("Vestel").add("SmartTV");
+ vp9WhiteList.get("wnc").add("c71kw400");
+ vp9WhiteList.get("Xiaomi").add("MIBOX3");
+ vp9WhiteList.get("ZTE TV").add("AV-ATB100");
+ vp9WhiteList.get("ZTE TV").add("B860H");
+
+ isVp9WhiteListed =
+ vp9WhiteList.containsKey(Build.BRAND)
+ && vp9WhiteList.get(Build.BRAND).contains(Build.MODEL);
+ }
+
+ private MediaCodecUtil() {}
+
+ /**
+ * Returns whether a given combination of (frame width x frame height) frames at bitrate and fps
+ * has a decoder with mime type.
+ *
+ * <p>Setting any of the int parameters to 0 indicates that they shouldn't be considered.
+ */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ public static boolean hasVideoDecoderFor(
+ String mimeType,
+ boolean secure,
+ int frameWidth,
+ int frameHeight,
+ int bitrate,
+ int fps,
+ boolean mustSupportHdr) {
+ FindVideoDecoderResult findVideoDecoderResult =
+ findVideoDecoder(mimeType, secure, frameWidth, frameHeight, bitrate, fps, mustSupportHdr);
+ return !findVideoDecoderResult.name.equals("")
+ && (!mustSupportHdr || isHdrCapableVp9Decoder(findVideoDecoderResult));
+ }
+
+ /**
+ * Returns whether an audio decoder that supports mimeType at bitrate. Setting bitrate to 0
+ * indicates that it should not be considered.
+ */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ public static boolean hasAudioDecoderFor(String mimeType, int bitrate) {
+ return !findAudioDecoder(mimeType, bitrate).equals("");
+ }
+
+ /** Determine whether the system has a decoder capable of playing HDR VP9. */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ public static boolean hasHdrCapableVp9Decoder() {
+ // VP9Profile* values were not added until API level 24. See
+ // https://developer.android.com/reference/android/media/MediaCodecInfo.CodecProfileLevel.html.
+ if (Build.VERSION.SDK_INT < 24) {
+ return false;
+ }
+
+ FindVideoDecoderResult findVideoDecoderResult =
+ findVideoDecoder(VP9_MIME_TYPE, false, 0, 0, 0, 0, true);
+ return isHdrCapableVp9Decoder(findVideoDecoderResult);
+ }
+
+ /** Determine whether findVideoDecoderResult is capable of playing HDR VP9 */
+ public static boolean isHdrCapableVp9Decoder(FindVideoDecoderResult findVideoDecoderResult) {
+ CodecCapabilities codecCapabilities = findVideoDecoderResult.codecCapabilities;
+ if (codecCapabilities == null) {
+ return false;
+ }
+ CodecProfileLevel[] codecProfileLevels = codecCapabilities.profileLevels;
+ if (codecProfileLevels == null) {
+ return false;
+ }
+ for (CodecProfileLevel codecProfileLevel : codecProfileLevels) {
+ if (codecProfileLevel.profile == CodecProfileLevel.VP9Profile2HDR
+ || codecProfileLevel.profile == CodecProfileLevel.VP9Profile3HDR) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * The same as hasVideoDecoderFor, only return the name of the video decoder if it is found, and
+ * "" otherwise.
+ */
+ public static FindVideoDecoderResult findVideoDecoder(
+ String mimeType,
+ boolean secure,
+ int frameWidth,
+ int frameHeight,
+ int bitrate,
+ int fps,
+ boolean hdr) {
+ Log.v(
+ TAG,
+ String.format(
+ "Searching for video decoder with parameters "
+ + "mimeType: %s, secure: %b, frameWidth: %d, frameHeight: %d, bitrate: %d, fps: %d",
+ mimeType, secure, frameWidth, frameHeight, bitrate, fps));
+ Log.v(
+ TAG,
+ String.format(
+ "brand: %s, model: %s, version: %s, API level: %d, isVp9WhiteListed: %b",
+ Build.BRAND,
+ Build.MODEL,
+ Build.VERSION.RELEASE,
+ Build.VERSION.SDK_INT,
+ isVp9WhiteListed));
+
+ // Note: MediaCodecList is sorted by the framework such that the best decoders come first.
+ for (MediaCodecInfo info : new MediaCodecList(MediaCodecList.ALL_CODECS).getCodecInfos()) {
+ if (info.isEncoder()) {
+ continue;
+ }
+ for (String supportedType : info.getSupportedTypes()) {
+ if (!supportedType.equalsIgnoreCase(mimeType)) {
+ continue;
+ }
+
+ String name = info.getName();
+ if (!isVp9WhiteListed && codecBlackList.contains(name)) {
+ Log.v(TAG, String.format("Rejecting %s, reason: codec is black listed", name));
+ continue;
+ }
+ // MediaCodecList is supposed to feed us names of decoders that do NOT end in ".secure". We
+ // are then supposed to check if FEATURE_SecurePlayback is supported, and it if is and we
+ // want a secure codec, we append ".secure" ourselves, and then pass that to
+ // MediaCodec.createDecoderByName. Some devices, do not follow this spec, and show us
+ // decoders that end in ".secure". Empirically, FEATURE_SecurePlayback has still been
+ // correct when this happens.
+ if (name.endsWith(SECURE_DECODER_SUFFIX)) {
+ // If we want a secure decoder, then make sure the version without ".secure" isn't
+ // blacklisted.
+ String nameWithoutSecureSuffix =
+ name.substring(0, name.length() - SECURE_DECODER_SUFFIX.length());
+ if (secure && !isVp9WhiteListed && codecBlackList.contains(nameWithoutSecureSuffix)) {
+ Log.v(
+ TAG,
+ String.format("Rejecting %s, reason: offpsec blacklisted secure decoder", name));
+ continue;
+ }
+ // If we don't want a secure decoder, then don't bother messing around with this thing.
+ if (!secure) {
+ Log.v(
+ TAG,
+ String.format(
+ "Rejecting %s, reason: want !secure decoder and ends with .secure", name));
+ continue;
+ }
+ }
+
+ CodecCapabilities codecCapabilities = info.getCapabilitiesForType(supportedType);
+ if (secure
+ && !codecCapabilities.isFeatureSupported(
+ MediaCodecInfo.CodecCapabilities.FEATURE_SecurePlayback)) {
+ Log.v(
+ TAG,
+ String.format(
+ "Rejecting %s, reason: want secure decoder and !FEATURE_SecurePlayback", name));
+ continue;
+ }
+
+ // VideoCapabilties is not implemented correctly on this device.
+ if (Build.VERSION.SDK_INT < 23
+ && Build.MODEL.equals("MIBOX3")
+ && name.equals("OMX.amlogic.vp9.decoder.awesome")
+ && (frameWidth > 1920 || frameHeight > 1080)) {
+ Log.v(TAG, "Skipping >1080p OMX.amlogic.vp9.decoder.awesome on mibox.");
+ continue;
+ }
+
+ VideoCapabilities videoCapabilities = codecCapabilities.getVideoCapabilities();
+ if (frameWidth != 0 && !videoCapabilities.getSupportedWidths().contains(frameWidth)) {
+ Log.v(
+ TAG,
+ String.format(
+ "Rejecting %s, reason: supported widths %s does not contain %d",
+ name, videoCapabilities.getSupportedWidths().toString(), frameWidth));
+ continue;
+ }
+ if (frameHeight != 0 && !videoCapabilities.getSupportedHeights().contains(frameHeight)) {
+ Log.v(
+ TAG,
+ String.format(
+ "Rejecting %s, reason: supported heights %s does not contain %d",
+ name, videoCapabilities.getSupportedHeights().toString(), frameHeight));
+ continue;
+ }
+ if (bitrate != 0 && !videoCapabilities.getBitrateRange().contains(bitrate)) {
+ Log.v(
+ TAG,
+ String.format(
+ "Rejecting %s, reason: bitrate range %s does not contain %d",
+ name, videoCapabilities.getBitrateRange().toString(), bitrate));
+ continue;
+ }
+ if (fps != 0 && !videoCapabilities.getSupportedFrameRates().contains(fps)) {
+ Log.v(
+ TAG,
+ String.format(
+ "Rejecting %s, reason: supported frame rates %s does not contain %d",
+ name, videoCapabilities.getSupportedFrameRates().toString(), fps));
+ continue;
+ }
+ String resultName =
+ (secure && !name.endsWith(SECURE_DECODER_SUFFIX))
+ ? (name + SECURE_DECODER_SUFFIX)
+ : name;
+ FindVideoDecoderResult findVideoDecoderResult =
+ new FindVideoDecoderResult(resultName, videoCapabilities, codecCapabilities);
+ if (hdr && !isHdrCapableVp9Decoder(findVideoDecoderResult)) {
+ continue;
+ }
+ Log.v(TAG, String.format("Found suitable decoder, %s", name));
+ return findVideoDecoderResult;
+ }
+ }
+ return new FindVideoDecoderResult("", null, null);
+ }
+
+ /**
+ * The same as hasAudioDecoderFor, only return the name of the audio decoder if it is found, and
+ * "" otherwise.
+ */
+ public static String findAudioDecoder(String mimeType, int bitrate) {
+ // Note: MediaCodecList is sorted by the framework such that the best decoders come first.
+ for (MediaCodecInfo info : new MediaCodecList(MediaCodecList.ALL_CODECS).getCodecInfos()) {
+ if (info.isEncoder()) {
+ continue;
+ }
+ for (String supportedType : info.getSupportedTypes()) {
+ if (!supportedType.equalsIgnoreCase(mimeType)) {
+ continue;
+ }
+ String name = info.getName();
+ AudioCapabilities audioCapabilities =
+ info.getCapabilitiesForType(supportedType).getAudioCapabilities();
+ if (bitrate != 0 && !audioCapabilities.getBitrateRange().contains(bitrate)) {
+ continue;
+ }
+ return name;
+ }
+ }
+ return "";
+ }
+
+ /**
+ * Debug utility function that can be locally added to dump information about all decoders on a
+ * particular system.
+ */
+ @SuppressWarnings("unused")
+ private static void dumpAllDecoders() {
+ for (MediaCodecInfo info : new MediaCodecList(MediaCodecList.ALL_CODECS).getCodecInfos()) {
+ if (info.isEncoder()) {
+ continue;
+ }
+ for (String supportedType : info.getSupportedTypes()) {
+ String name = info.getName();
+ CodecCapabilities codecCapabilities = info.getCapabilitiesForType(supportedType);
+ Log.v(TAG, "==================================================");
+ Log.v(TAG, String.format("name: %s", name));
+ Log.v(TAG, String.format("supportedType: %s", supportedType));
+ Log.v(
+ TAG, String.format("codecBlackList.contains(name): %b", codecBlackList.contains(name)));
+ Log.v(
+ TAG,
+ String.format(
+ "FEATURE_SecurePlayback: %b",
+ codecCapabilities.isFeatureSupported(
+ MediaCodecInfo.CodecCapabilities.FEATURE_SecurePlayback)));
+ VideoCapabilities videoCapabilities = codecCapabilities.getVideoCapabilities();
+ if (videoCapabilities != null) {
+ Log.v(
+ TAG,
+ String.format(
+ "videoCapabilities.getSupportedWidths(): %s",
+ videoCapabilities.getSupportedWidths().toString()));
+ Log.v(
+ TAG,
+ String.format(
+ "videoCapabilities.getSupportedHeights(): %s",
+ videoCapabilities.getSupportedHeights().toString()));
+ Log.v(
+ TAG,
+ String.format(
+ "videoCapabilities.getBitrateRange(): %s",
+ videoCapabilities.getBitrateRange().toString()));
+ Log.v(
+ TAG,
+ String.format(
+ "videoCapabilities.getSupportedFrameRates(): %s",
+ videoCapabilities.getSupportedFrameRates().toString()));
+ }
+ Log.v(TAG, "==================================================");
+ Log.v(TAG, "");
+ }
+ }
+ }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaDrmBridge.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaDrmBridge.java
new file mode 100644
index 0000000..4619b57
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaDrmBridge.java
@@ -0,0 +1,661 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+// Modifications Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.media;
+
+import static dev.cobalt.media.Log.TAG;
+
+import android.annotation.TargetApi;
+import android.media.DeniedByServerException;
+import android.media.MediaCrypto;
+import android.media.MediaCryptoException;
+import android.media.MediaDrm;
+import android.media.MediaDrm.OnEventListener;
+import android.media.MediaDrmException;
+import android.media.NotProvisionedException;
+import android.media.UnsupportedSchemeException;
+import android.os.Build;
+import dev.cobalt.coat.CobaltHttpHelper;
+import dev.cobalt.util.Log;
+import dev.cobalt.util.UsedByNative;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.UUID;
+
+/** A wrapper of the android MediaDrm class. */
+@UsedByNative
+public class MediaDrmBridge {
+ // Implementation Notes:
+ // - A media crypto session (mMediaCryptoSession) is opened after MediaDrm
+ // is created. This session will NOT be added to mSessionIds and will only
+ // be used to create the MediaCrypto object.
+ // - Each createSession() call creates a new session. All created sessions
+ // are managed in mSessionIds.
+ // - Whenever NotProvisionedException is thrown, we will clean up the
+ // current state and start the provisioning process.
+ // - When provisioning is finished, we will try to resume suspended
+ // operations:
+ // a) Create the media crypto session if it's not created.
+ // b) Finish createSession() if previous createSession() was interrupted
+ // by a NotProvisionedException.
+ // - Whenever an unexpected error occurred, we'll call release() to release
+ // all resources immediately, clear all states and fail all pending
+ // operations. After that all calls to this object will fail (e.g. return
+ // null or reject the promise). All public APIs and callbacks should check
+ // mMediaBridge to make sure release() hasn't been called.
+
+ private static final char[] HEX_CHAR_LOOKUP = "0123456789ABCDEF".toCharArray();
+ private static final long INVALID_NATIVE_MEDIA_DRM_BRIDGE = 0;
+
+ // The value of this must stay in sync with kSbDrmTicketInvalid in "starboard/drm.h"
+ private static final int SB_DRM_TICKET_INVALID = Integer.MIN_VALUE;
+
+ // Scheme UUID for Widevine. See http://dashif.org/identifiers/protection/
+ private static final UUID WIDEVINE_UUID = UUID.fromString("edef8ba9-79d6-4ace-a3c8-27dcd51d21ed");
+
+ // Deprecated in API 26, but we still log it on earlier devices.
+ // We do handle STATUS_EXPIRED in nativeOnKeyStatusChange() for API 23+ devices.
+ @SuppressWarnings("deprecation")
+ private static final int MEDIA_DRM_EVENT_KEY_EXPIRED = MediaDrm.EVENT_KEY_EXPIRED;
+
+ private MediaDrm mMediaDrm;
+ private long mNativeMediaDrmBridge;
+ private UUID mSchemeUUID;
+
+ // A session only for the purpose of creating a MediaCrypto object. Created
+ // after construction, or after the provisioning process is successfully
+ // completed. No getKeyRequest() should be called on |mMediaCryptoSession|.
+ private byte[] mMediaCryptoSession;
+
+ // The map of all opened sessions (excluding mMediaCryptoSession) to their
+ // mime types.
+ private HashMap<ByteBuffer, String> mSessionIds = new HashMap<>();
+
+ private MediaCrypto mMediaCrypto;
+
+ /**
+ * Create a new MediaDrmBridge with the Widevine crypto scheme.
+ *
+ * @param nativeMediaDrmBridge The native owner of this class.
+ */
+ @UsedByNative
+ static MediaDrmBridge create(long nativeMediaDrmBridge) {
+ UUID cryptoScheme = WIDEVINE_UUID;
+ if (!MediaDrm.isCryptoSchemeSupported(cryptoScheme)) {
+ return null;
+ }
+
+ MediaDrmBridge mediaDrmBridge = null;
+ try {
+ mediaDrmBridge = new MediaDrmBridge(cryptoScheme, nativeMediaDrmBridge);
+ Log.d(TAG, "MediaDrmBridge successfully created.");
+ } catch (UnsupportedSchemeException e) {
+ Log.e(TAG, "Unsupported DRM scheme", e);
+ return null;
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Failed to create MediaDrmBridge", e);
+ return null;
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Failed to create MediaDrmBridge", e);
+ return null;
+ }
+
+ if (!mediaDrmBridge.createMediaCrypto()) {
+ return null;
+ }
+
+ return mediaDrmBridge;
+ }
+
+ /**
+ * Check whether the Widevine crypto scheme is supported.
+ *
+ * @return true if the container and the crypto scheme is supported, or false otherwise.
+ */
+ @UsedByNative
+ static boolean isWidevineCryptoSchemeSupported() {
+ return MediaDrm.isCryptoSchemeSupported(WIDEVINE_UUID);
+ }
+
+ /**
+ * Check whether the Widevine crypto scheme is supported for the given container. If
+ * |containerMimeType| is an empty string, we just return whether the crypto scheme is supported.
+ *
+ * @return true if the container and the crypto scheme is supported, or false otherwise.
+ */
+ @UsedByNative
+ static boolean isWidevineCryptoSchemeSupported(String containerMimeType) {
+ if (containerMimeType.isEmpty()) {
+ return isWidevineCryptoSchemeSupported();
+ }
+ return MediaDrm.isCryptoSchemeSupported(WIDEVINE_UUID, containerMimeType);
+ }
+
+ /** Destroy the MediaDrmBridge object. */
+ @UsedByNative
+ void destroy() {
+ mNativeMediaDrmBridge = INVALID_NATIVE_MEDIA_DRM_BRIDGE;
+ if (mMediaDrm != null) {
+ release();
+ }
+ }
+
+ @UsedByNative
+ void createSession(int ticket, byte[] initData, String mime) {
+ Log.d(TAG, "createSession()");
+
+ if (mMediaDrm == null) {
+ Log.e(TAG, "createSession() called when MediaDrm is null.");
+ return;
+ }
+
+ boolean newSessionOpened = false;
+ byte[] sessionId = null;
+ try {
+ sessionId = openSession();
+ if (sessionId == null) {
+ Log.e(TAG, "Open session failed.");
+ return;
+ }
+ newSessionOpened = true;
+ if (sessionExists(sessionId)) {
+ Log.e(TAG, "Opened session that already exists.");
+ return;
+ }
+
+ MediaDrm.KeyRequest request = null;
+ request = getKeyRequest(sessionId, initData, mime);
+ if (request == null) {
+ try {
+ // Some implementations let this method throw exceptions.
+ mMediaDrm.closeSession(sessionId);
+ } catch (Exception e) {
+ Log.e(TAG, "closeSession failed", e);
+ }
+ Log.e(TAG, "Generate request failed.");
+ return;
+ }
+
+ // Success!
+ Log.d(
+ TAG,
+ String.format("createSession(): Session (%s) created.", bytesToHexString(sessionId)));
+ mSessionIds.put(ByteBuffer.wrap(sessionId), mime);
+ onSessionMessage(ticket, sessionId, request);
+ } catch (NotProvisionedException e) {
+ Log.e(TAG, "Device not provisioned", e);
+ if (newSessionOpened) {
+ try {
+ // Some implementations let this method throw exceptions.
+ mMediaDrm.closeSession(sessionId);
+ } catch (Exception ex) {
+ Log.e(TAG, "closeSession failed", ex);
+ }
+ }
+ attemptProvisioning();
+ }
+ }
+
+ /**
+ * Update a session with response.
+ *
+ * @param sessionId Reference ID of session to be updated.
+ * @param response Response data from the server.
+ */
+ @UsedByNative
+ boolean updateSession(byte[] sessionId, byte[] response) {
+ Log.d(TAG, "updateSession()");
+ if (mMediaDrm == null) {
+ Log.e(TAG, "updateSession() called when MediaDrm is null.");
+ return false;
+ }
+
+ if (!sessionExists(sessionId)) {
+ Log.e(TAG, "updateSession tried to update a session that does not exist.");
+ return false;
+ }
+
+ try {
+ try {
+ mMediaDrm.provideKeyResponse(sessionId, response);
+ } catch (IllegalStateException e) {
+ // This is not really an exception. Some error codes are incorrectly
+ // reported as an exception.
+ Log.e(TAG, "Exception intentionally caught when calling provideKeyResponse()", e);
+ }
+ Log.d(
+ TAG, String.format("Key successfully added for session %s", bytesToHexString(sessionId)));
+ if (Build.VERSION.SDK_INT < 23) {
+ // Pass null to indicate that KeyStatus isn't supported.
+ nativeOnKeyStatusChange(mNativeMediaDrmBridge, sessionId, null);
+ }
+ return true;
+ } catch (NotProvisionedException e) {
+ // TODO: Should we handle this?
+ Log.e(TAG, "failed to provide key response", e);
+ } catch (DeniedByServerException e) {
+ Log.e(TAG, "failed to provide key response", e);
+ }
+ Log.e(TAG, "Update session failed.");
+ release();
+ return false;
+ }
+
+ /**
+ * Close a session that was previously created by createSession().
+ *
+ * @param sessionId ID of session to be closed.
+ */
+ @UsedByNative
+ void closeSession(byte[] sessionId) {
+ Log.d(TAG, "closeSession()");
+ if (mMediaDrm == null) {
+ Log.e(TAG, "closeSession() called when MediaDrm is null.");
+ return;
+ }
+
+ if (!sessionExists(sessionId)) {
+ Log.e(TAG, "Invalid sessionId in closeSession(): " + bytesToHexString(sessionId));
+ return;
+ }
+
+ try {
+ // Some implementations don't have removeKeys.
+ // https://bugs.chromium.org/p/chromium/issues/detail?id=475632
+ mMediaDrm.removeKeys(sessionId);
+ } catch (Exception e) {
+ Log.e(TAG, "removeKeys failed: ", e);
+ }
+ try {
+ // Some implementations let this method throw exceptions.
+ mMediaDrm.closeSession(sessionId);
+ } catch (Exception e) {
+ Log.e(TAG, "closeSession failed: ", e);
+ }
+ mSessionIds.remove(ByteBuffer.wrap(sessionId));
+ Log.d(TAG, String.format("Session %s closed", bytesToHexString(sessionId)));
+ }
+
+ @UsedByNative
+ MediaCrypto getMediaCrypto() {
+ return mMediaCrypto;
+ }
+
+ private MediaDrmBridge(UUID schemeUUID, long nativeMediaDrmBridge)
+ throws android.media.UnsupportedSchemeException {
+ mSchemeUUID = schemeUUID;
+ mMediaDrm = new MediaDrm(schemeUUID);
+
+ mNativeMediaDrmBridge = nativeMediaDrmBridge;
+ if (!isNativeMediaDrmBridgeValid()) {
+ throw new IllegalArgumentException(
+ String.format("Invalid nativeMediaDrmBridge value: |%d|.", nativeMediaDrmBridge));
+ }
+
+ mMediaDrm.setOnEventListener(
+ new OnEventListener() {
+ @Override
+ public void onEvent(MediaDrm md, byte[] sessionId, int event, int extra, byte[] data) {
+ if (sessionId == null) {
+ Log.e(TAG, "EventListener: Null session.");
+ return;
+ }
+ if (!sessionExists(sessionId)) {
+ Log.e(
+ TAG,
+ String.format("EventListener: Invalid session %s", bytesToHexString(sessionId)));
+ return;
+ }
+ switch (event) {
+ case MediaDrm.EVENT_KEY_REQUIRED:
+ Log.d(TAG, "MediaDrm.EVENT_KEY_REQUIRED");
+ String mime = mSessionIds.get(ByteBuffer.wrap(sessionId));
+ MediaDrm.KeyRequest request = null;
+ try {
+ request = getKeyRequest(sessionId, data, mime);
+ } catch (NotProvisionedException e) {
+ Log.e(TAG, "Device not provisioned", e);
+ if (!attemptProvisioning()) {
+ Log.e(TAG, "Failed to provision device when responding to EVENT_KEY_REQUIRED");
+ return;
+ }
+ // If we supposedly successfully provisioned ourselves, then try to create a
+ // request again.
+ try {
+ request = getKeyRequest(sessionId, data, mime);
+ } catch (NotProvisionedException e2) {
+ Log.e(
+ TAG,
+ "Device still not provisioned after supposedly successful provisioning",
+ e2);
+ return;
+ }
+ }
+ if (request != null) {
+ onSessionMessage(SB_DRM_TICKET_INVALID, sessionId, request);
+ } else {
+ Log.e(TAG, "EventListener: getKeyRequest failed.");
+ return;
+ }
+ break;
+ case MEDIA_DRM_EVENT_KEY_EXPIRED:
+ Log.d(TAG, "MediaDrm.EVENT_KEY_EXPIRED");
+ break;
+ case MediaDrm.EVENT_VENDOR_DEFINED:
+ Log.d(TAG, "MediaDrm.EVENT_VENDOR_DEFINED");
+ break;
+ default:
+ Log.e(TAG, "Invalid DRM event " + event);
+ return;
+ }
+ }
+ });
+
+ if (Build.VERSION.SDK_INT >= 23) {
+ setOnKeyStatusChangeListenerV23();
+ }
+
+ mMediaDrm.setPropertyString("privacyMode", "enable");
+ mMediaDrm.setPropertyString("sessionSharing", "enable");
+ }
+
+ @TargetApi(23)
+ private void setOnKeyStatusChangeListenerV23() {
+ mMediaDrm.setOnKeyStatusChangeListener(
+ new MediaDrm.OnKeyStatusChangeListener() {
+ @Override
+ public void onKeyStatusChange(
+ MediaDrm md,
+ byte[] sessionId,
+ List<MediaDrm.KeyStatus> keyInformation,
+ boolean hasNewUsableKey) {
+ nativeOnKeyStatusChange(
+ mNativeMediaDrmBridge,
+ sessionId,
+ keyInformation.toArray(new MediaDrm.KeyStatus[keyInformation.size()]));
+ }
+ },
+ null);
+ }
+
+ /** Convert byte array to hex string for logging. */
+ private static String bytesToHexString(byte[] bytes) {
+ StringBuilder hexString = new StringBuilder();
+ for (int i = 0; i < bytes.length; ++i) {
+ hexString.append(HEX_CHAR_LOOKUP[bytes[i] >>> 4]);
+ hexString.append(HEX_CHAR_LOOKUP[bytes[i] & 0xf]);
+ }
+ return hexString.toString();
+ }
+
+ private void onSessionMessage(
+ int ticket, final byte[] sessionId, final MediaDrm.KeyRequest request) {
+ if (!isNativeMediaDrmBridgeValid()) {
+ return;
+ }
+
+ int requestType = MediaDrm.KeyRequest.REQUEST_TYPE_INITIAL;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ requestType = request.getRequestType();
+ } else {
+ // Prior to M, getRequestType() is not supported. Do our best guess here: Assume
+ // requests with a URL are renewals and all others are initial requests.
+ requestType =
+ request.getDefaultUrl().isEmpty()
+ ? MediaDrm.KeyRequest.REQUEST_TYPE_INITIAL
+ : MediaDrm.KeyRequest.REQUEST_TYPE_RENEWAL;
+ }
+
+ nativeOnSessionMessage(
+ mNativeMediaDrmBridge, ticket, sessionId, requestType, request.getData());
+ }
+
+ /**
+ * Get a key request.
+ *
+ * @param sessionId ID of session on which we need to get the key request.
+ * @param data Data needed to get the key request.
+ * @param mime Mime type to get the key request.
+ * @return the key request.
+ */
+ private MediaDrm.KeyRequest getKeyRequest(byte[] sessionId, byte[] data, String mime)
+ throws android.media.NotProvisionedException {
+ if (mMediaDrm == null) {
+ throw new IllegalStateException("mMediaDrm cannot be null in getKeyRequest");
+ }
+ if (mMediaCryptoSession == null) {
+ throw new IllegalStateException("mMediaCryptoSession cannot be null in getKeyRequest.");
+ }
+ // TODO: Cannot do this during provisioning pending.
+
+ HashMap<String, String> optionalParameters = new HashMap<>();
+ MediaDrm.KeyRequest request = null;
+ try {
+ request =
+ mMediaDrm.getKeyRequest(
+ sessionId, data, mime, MediaDrm.KEY_TYPE_STREAMING, optionalParameters);
+ } catch (IllegalStateException e) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
+ && e instanceof android.media.MediaDrm.MediaDrmStateException) {
+ Log.e(TAG, "MediaDrmStateException fired during getKeyRequest().", e);
+ }
+ }
+
+ String result = (request != null) ? "succeeded" : "failed";
+ Log.d(TAG, String.format("getKeyRequest %s!", result));
+
+ return request;
+ }
+
+ /**
+ * Create a MediaCrypto object.
+ *
+ * @return false upon fatal error in creating MediaCrypto. Returns true otherwise, including the
+ * following two cases: 1. MediaCrypto is successfully created and notified. 2. Device is not
+ * provisioned and MediaCrypto creation will be tried again after the provisioning process is
+ * completed.
+ * <p>When false is returned, the caller should call release(), which will notify the native
+ * code with a null MediaCrypto, if needed.
+ */
+ private boolean createMediaCrypto() {
+ if (mMediaDrm == null) {
+ throw new IllegalStateException("Cannot create media crypto with null mMediaDrm.");
+ }
+ if (mMediaCryptoSession != null) {
+ throw new IllegalStateException(
+ "Cannot create media crypto with non-null mMediaCryptoSession.");
+ }
+ // TODO: Cannot do this during provisioning pending.
+
+ // Open media crypto session.
+ try {
+ mMediaCryptoSession = openSession();
+ } catch (NotProvisionedException e) {
+ Log.d(TAG, "Device not provisioned", e);
+ if (!attemptProvisioning()) {
+ Log.e(TAG, "Failed to provision device during MediaCrypto creation.");
+ return false;
+ }
+ return true;
+ }
+
+ if (mMediaCryptoSession == null) {
+ Log.e(TAG, "Cannot create MediaCrypto Session.");
+ return false;
+ }
+ Log.d(
+ TAG,
+ String.format("MediaCrypto Session created: %s", bytesToHexString(mMediaCryptoSession)));
+
+ // Create MediaCrypto object.
+ try {
+ if (MediaCrypto.isCryptoSchemeSupported(mSchemeUUID)) {
+ MediaCrypto mediaCrypto = new MediaCrypto(mSchemeUUID, mMediaCryptoSession);
+ Log.d(TAG, "MediaCrypto successfully created!");
+ mMediaCrypto = mediaCrypto;
+ return true;
+ } else {
+ Log.e(TAG, "Cannot create MediaCrypto for unsupported scheme.");
+ }
+ } catch (MediaCryptoException e) {
+ Log.e(TAG, "Cannot create MediaCrypto", e);
+ }
+
+ try {
+ // Some implementations let this method throw exceptions.
+ mMediaDrm.closeSession(mMediaCryptoSession);
+ } catch (Exception e) {
+ Log.e(TAG, "closeSession failed: ", e);
+ }
+ mMediaCryptoSession = null;
+
+ return false;
+ }
+
+ /**
+ * Open a new session.
+ *
+ * @return ID of the session opened. Returns null if unexpected error happened.
+ */
+ private byte[] openSession() throws android.media.NotProvisionedException {
+ Log.d(TAG, "openSession()");
+ if (mMediaDrm == null) {
+ throw new IllegalStateException("mMediaDrm cannot be null in openSession");
+ }
+ try {
+ byte[] sessionId = mMediaDrm.openSession();
+ // Make a clone here in case the underlying byte[] is modified.
+ return sessionId.clone();
+ } catch (RuntimeException e) { // TODO: Drop this?
+ Log.e(TAG, "Cannot open a new session", e);
+ release();
+ return null;
+ } catch (NotProvisionedException e) {
+ // Throw NotProvisionedException so that we can attemptProvisioning().
+ throw e;
+ } catch (MediaDrmException e) {
+ // Other MediaDrmExceptions (e.g. ResourceBusyException) are not
+ // recoverable.
+ Log.e(TAG, "Cannot open a new session", e);
+ release();
+ return null;
+ }
+ }
+
+ /**
+ * Attempt to get the device that we are currently running on provisioned.
+ *
+ * @return whether provisioning was successful or not.
+ */
+ private boolean attemptProvisioning() {
+ Log.d(TAG, "attemptProvisioning()");
+ MediaDrm.ProvisionRequest request = mMediaDrm.getProvisionRequest();
+ String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData());
+ byte[] response = new CobaltHttpHelper().performDrmHttpPost(url);
+ if (response == null) {
+ return false;
+ }
+ try {
+ mMediaDrm.provideProvisionResponse(response);
+ return true;
+ } catch (android.media.DeniedByServerException e) {
+ Log.e(TAG, "failed to provide provision response", e);
+ } catch (java.lang.IllegalStateException e) {
+ Log.e(TAG, "failed to provide provision response", e);
+ }
+ return false;
+ }
+
+ /**
+ * Check whether |sessionId| is an existing session ID, excluding the media crypto session.
+ *
+ * @param sessionId Crypto session Id.
+ * @return true if |sessionId| exists, false otherwise.
+ */
+ private boolean sessionExists(byte[] sessionId) {
+ if (mMediaCryptoSession == null) {
+ if (!mSessionIds.isEmpty()) {
+ throw new IllegalStateException(
+ "mSessionIds must be empty if crypto session does not exist.");
+ }
+ Log.e(TAG, "Session doesn't exist because media crypto session is not created.");
+ return false;
+ }
+ return !Arrays.equals(sessionId, mMediaCryptoSession)
+ && mSessionIds.containsKey(ByteBuffer.wrap(sessionId));
+ }
+
+ /** Release all allocated resources and finish all pending operations. */
+ private void release() {
+ // Note that mNativeMediaDrmBridge may have already been reset (see destroy()).
+ if (mMediaDrm == null) {
+ throw new IllegalStateException("Called release with null mMediaDrm.");
+ }
+
+ // Close all open sessions.
+ for (ByteBuffer sessionId : mSessionIds.keySet()) {
+ try {
+ // Some implementations don't have removeKeys.
+ // https://bugs.chromium.org/p/chromium/issues/detail?id=475632
+ mMediaDrm.removeKeys(sessionId.array());
+ } catch (Exception e) {
+ Log.e(TAG, "removeKeys failed: ", e);
+ }
+
+ try {
+ // Some implementations let this method throw exceptions.
+ mMediaDrm.closeSession(sessionId.array());
+ } catch (Exception e) {
+ Log.e(TAG, "closeSession failed: ", e);
+ }
+ Log.d(
+ TAG,
+ String.format("Successfully closed session (%s)", bytesToHexString(sessionId.array())));
+ }
+ mSessionIds.clear();
+ mSessionIds = null;
+
+ // Close mMediaCryptoSession if it's open.
+ if (mMediaCryptoSession != null) {
+ try {
+ // Some implementations let this method throw exceptions.
+ mMediaDrm.closeSession(mMediaCryptoSession);
+ } catch (Exception e) {
+ Log.e(TAG, "closeSession failed: ", e);
+ }
+ mMediaCryptoSession = null;
+ }
+
+ if (mMediaDrm != null) {
+ mMediaDrm.release();
+ mMediaDrm = null;
+ }
+ }
+
+ private boolean isNativeMediaDrmBridgeValid() {
+ return mNativeMediaDrmBridge != INVALID_NATIVE_MEDIA_DRM_BRIDGE;
+ }
+
+ private native void nativeOnSessionMessage(
+ long nativeMediaDrmBridge, int ticket, byte[] sessionId, int requestType, byte[] message);
+
+ private native void nativeOnKeyStatusChange(
+ long nativeMediaDrmBridge, byte[] sessionId, MediaDrm.KeyStatus[] keyInformation);
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaImage.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaImage.java
new file mode 100644
index 0000000..899dc22
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaImage.java
@@ -0,0 +1,31 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.media;
+
+import dev.cobalt.util.UsedByNative;
+
+/** https://wicg.github.io/mediasession/#dictdef-mediaimage */
+public class MediaImage {
+ public final String src;
+ public final String sizes;
+ public final String type;
+
+ @UsedByNative
+ public MediaImage(String src, String sizes, String type) {
+ this.src = src;
+ this.sizes = sizes;
+ this.type = type;
+ }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/VideoFrameReleaseTimeHelper.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/VideoFrameReleaseTimeHelper.java
new file mode 100644
index 0000000..4287c8c
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/VideoFrameReleaseTimeHelper.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// Modifications Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.media;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.view.Choreographer;
+import android.view.Choreographer.FrameCallback;
+import android.view.WindowManager;
+import dev.cobalt.util.UsedByNative;
+
+/** Makes a best effort to adjust frame release timestamps for a smoother visual result. */
+@TargetApi(16)
+@SuppressWarnings("unused")
+@UsedByNative
+public final class VideoFrameReleaseTimeHelper {
+
+ private static final double DISPLAY_REFRESH_RATE_UNKNOWN = -1;
+ private static final long CHOREOGRAPHER_SAMPLE_DELAY_MILLIS = 500;
+ private static final long MAX_ALLOWED_DRIFT_NS = 20000000;
+
+ private static final long VSYNC_OFFSET_PERCENTAGE = 80;
+ private static final int MIN_FRAMES_FOR_ADJUSTMENT = 6;
+ private static final long NANOS_PER_SECOND = 1000000000L;
+
+ private final VSyncSampler vsyncSampler;
+ private final boolean useDefaultDisplayVsync;
+ private final long vsyncDurationNs;
+ private final long vsyncOffsetNs;
+
+ private long lastFramePresentationTimeUs;
+ private long adjustedLastFrameTimeNs;
+ private long pendingAdjustedFrameTimeNs;
+
+ private boolean haveSync;
+ private long syncUnadjustedReleaseTimeNs;
+ private long syncFramePresentationTimeNs;
+ private long frameCount;
+
+ /**
+ * Constructs an instance that smooths frame release timestamps but does not align them with the
+ * default display's vsync signal.
+ */
+ @SuppressWarnings("unused")
+ public VideoFrameReleaseTimeHelper() {
+ this(DISPLAY_REFRESH_RATE_UNKNOWN);
+ }
+
+ /**
+ * Constructs an instance that smooths frame release timestamps and aligns them with the default
+ * display's vsync signal.
+ *
+ * @param context A context from which information about the default display can be retrieved.
+ */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ public VideoFrameReleaseTimeHelper(Context context) {
+ this(getDefaultDisplayRefreshRate(context));
+ }
+
+ private VideoFrameReleaseTimeHelper(double defaultDisplayRefreshRate) {
+ useDefaultDisplayVsync = defaultDisplayRefreshRate != DISPLAY_REFRESH_RATE_UNKNOWN;
+ if (useDefaultDisplayVsync) {
+ vsyncSampler = VSyncSampler.getInstance();
+ vsyncDurationNs = (long) (NANOS_PER_SECOND / defaultDisplayRefreshRate);
+ vsyncOffsetNs = (vsyncDurationNs * VSYNC_OFFSET_PERCENTAGE) / 100;
+ } else {
+ vsyncSampler = null;
+ vsyncDurationNs = -1; // Value unused.
+ vsyncOffsetNs = -1; // Value unused.
+ }
+ }
+
+ /** Enables the helper. */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ public void enable() {
+ haveSync = false;
+ if (useDefaultDisplayVsync) {
+ vsyncSampler.addObserver();
+ }
+ }
+
+ /** Disables the helper. */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ public void disable() {
+ if (useDefaultDisplayVsync) {
+ vsyncSampler.removeObserver();
+ }
+ }
+
+ /**
+ * Adjusts a frame release timestamp.
+ *
+ * @param framePresentationTimeUs The frame's presentation time, in microseconds.
+ * @param unadjustedReleaseTimeNs The frame's unadjusted release time, in nanoseconds and in the
+ * same time base as {@link System#nanoTime()}.
+ * @return The adjusted frame release timestamp, in nanoseconds and in the same time base as
+ * {@link System#nanoTime()}.
+ */
+ @SuppressWarnings("unused")
+ @UsedByNative
+ public long adjustReleaseTime(long framePresentationTimeUs, long unadjustedReleaseTimeNs) {
+ long framePresentationTimeNs = framePresentationTimeUs * 1000;
+
+ // Until we know better, the adjustment will be a no-op.
+ long adjustedFrameTimeNs = framePresentationTimeNs;
+ long adjustedReleaseTimeNs = unadjustedReleaseTimeNs;
+
+ if (haveSync) {
+ // See if we've advanced to the next frame.
+ if (framePresentationTimeUs != lastFramePresentationTimeUs) {
+ frameCount++;
+ adjustedLastFrameTimeNs = pendingAdjustedFrameTimeNs;
+ }
+ if (frameCount >= MIN_FRAMES_FOR_ADJUSTMENT) {
+ // We're synced and have waited the required number of frames to apply an adjustment.
+ // Calculate the average frame time across all the frames we've seen since the last sync.
+ // This will typically give us a frame rate at a finer granularity than the frame times
+ // themselves (which often only have millisecond granularity).
+ long averageFrameDurationNs =
+ (framePresentationTimeNs - syncFramePresentationTimeNs) / frameCount;
+ // Project the adjusted frame time forward using the average.
+ long candidateAdjustedFrameTimeNs = adjustedLastFrameTimeNs + averageFrameDurationNs;
+
+ if (isDriftTooLarge(candidateAdjustedFrameTimeNs, unadjustedReleaseTimeNs)) {
+ haveSync = false;
+ } else {
+ adjustedFrameTimeNs = candidateAdjustedFrameTimeNs;
+ adjustedReleaseTimeNs =
+ syncUnadjustedReleaseTimeNs + adjustedFrameTimeNs - syncFramePresentationTimeNs;
+ }
+ } else {
+ // We're synced but haven't waited the required number of frames to apply an adjustment.
+ // Check drift anyway.
+ if (isDriftTooLarge(framePresentationTimeNs, unadjustedReleaseTimeNs)) {
+ haveSync = false;
+ }
+ }
+ }
+
+ // If we need to sync, do so now.
+ if (!haveSync) {
+ syncFramePresentationTimeNs = framePresentationTimeNs;
+ syncUnadjustedReleaseTimeNs = unadjustedReleaseTimeNs;
+ frameCount = 0;
+ haveSync = true;
+ onSynced();
+ }
+
+ lastFramePresentationTimeUs = framePresentationTimeUs;
+ pendingAdjustedFrameTimeNs = adjustedFrameTimeNs;
+
+ if (vsyncSampler == null || vsyncSampler.sampledVsyncTimeNs == 0) {
+ return adjustedReleaseTimeNs;
+ }
+
+ // Find the timestamp of the closest vsync. This is the vsync that we're targeting.
+ long snappedTimeNs =
+ closestVsync(adjustedReleaseTimeNs, vsyncSampler.sampledVsyncTimeNs, vsyncDurationNs);
+ // Apply an offset so that we release before the target vsync, but after the previous one.
+ return snappedTimeNs - vsyncOffsetNs;
+ }
+
+ protected void onSynced() {
+ // Do nothing.
+ }
+
+ private boolean isDriftTooLarge(long frameTimeNs, long releaseTimeNs) {
+ long elapsedFrameTimeNs = frameTimeNs - syncFramePresentationTimeNs;
+ long elapsedReleaseTimeNs = releaseTimeNs - syncUnadjustedReleaseTimeNs;
+ return Math.abs(elapsedReleaseTimeNs - elapsedFrameTimeNs) > MAX_ALLOWED_DRIFT_NS;
+ }
+
+ private static long closestVsync(long releaseTime, long sampledVsyncTime, long vsyncDuration) {
+ long vsyncCount = (releaseTime - sampledVsyncTime) / vsyncDuration;
+ long snappedTimeNs = sampledVsyncTime + (vsyncDuration * vsyncCount);
+ long snappedBeforeNs;
+ long snappedAfterNs;
+ if (releaseTime <= snappedTimeNs) {
+ snappedBeforeNs = snappedTimeNs - vsyncDuration;
+ snappedAfterNs = snappedTimeNs;
+ } else {
+ snappedBeforeNs = snappedTimeNs;
+ snappedAfterNs = snappedTimeNs + vsyncDuration;
+ }
+ long snappedAfterDiff = snappedAfterNs - releaseTime;
+ long snappedBeforeDiff = releaseTime - snappedBeforeNs;
+ return snappedAfterDiff < snappedBeforeDiff ? snappedAfterNs : snappedBeforeNs;
+ }
+
+ private static double getDefaultDisplayRefreshRate(Context context) {
+ WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ return manager.getDefaultDisplay() != null
+ ? manager.getDefaultDisplay().getRefreshRate()
+ : DISPLAY_REFRESH_RATE_UNKNOWN;
+ }
+
+ /**
+ * Samples display vsync timestamps. A single instance using a single {@link Choreographer} is
+ * shared by all {@link VideoFrameReleaseTimeHelper} instances. This is done to avoid a resource
+ * leak in the platform on API levels prior to 23.
+ */
+ private static final class VSyncSampler implements FrameCallback, Handler.Callback {
+
+ public volatile long sampledVsyncTimeNs;
+
+ private static final int CREATE_CHOREOGRAPHER = 0;
+ private static final int MSG_ADD_OBSERVER = 1;
+ private static final int MSG_REMOVE_OBSERVER = 2;
+
+ private static final VSyncSampler INSTANCE = new VSyncSampler();
+
+ private final Handler handler;
+ private final HandlerThread choreographerOwnerThread;
+ private Choreographer choreographer;
+ private int observerCount;
+
+ public static VSyncSampler getInstance() {
+ return INSTANCE;
+ }
+
+ private VSyncSampler() {
+ choreographerOwnerThread = new HandlerThread("ChoreographerOwner:Handler");
+ choreographerOwnerThread.start();
+ handler = new Handler(choreographerOwnerThread.getLooper(), this);
+ handler.sendEmptyMessage(CREATE_CHOREOGRAPHER);
+ }
+
+ /**
+ * Notifies the sampler that a {@link VideoFrameReleaseTimeHelper} is observing {@link
+ * #sampledVsyncTimeNs}, and hence that the value should be periodically updated.
+ */
+ public void addObserver() {
+ handler.sendEmptyMessage(MSG_ADD_OBSERVER);
+ }
+
+ /**
+ * Notifies the sampler that a {@link VideoFrameReleaseTimeHelper} is no longer observing {@link
+ * #sampledVsyncTimeNs}.
+ */
+ public void removeObserver() {
+ handler.sendEmptyMessage(MSG_REMOVE_OBSERVER);
+ }
+
+ @Override
+ public void doFrame(long vsyncTimeNs) {
+ sampledVsyncTimeNs = vsyncTimeNs;
+ choreographer.postFrameCallbackDelayed(this, CHOREOGRAPHER_SAMPLE_DELAY_MILLIS);
+ }
+
+ @Override
+ public boolean handleMessage(Message message) {
+ switch (message.what) {
+ case CREATE_CHOREOGRAPHER:
+ {
+ createChoreographerInstanceInternal();
+ return true;
+ }
+ case MSG_ADD_OBSERVER:
+ {
+ addObserverInternal();
+ return true;
+ }
+ case MSG_REMOVE_OBSERVER:
+ {
+ removeObserverInternal();
+ return true;
+ }
+ default:
+ {
+ return false;
+ }
+ }
+ }
+
+ private void createChoreographerInstanceInternal() {
+ choreographer = Choreographer.getInstance();
+ }
+
+ private void addObserverInternal() {
+ observerCount++;
+ if (observerCount == 1) {
+ choreographer.postFrameCallback(this);
+ }
+ }
+
+ private void removeObserverInternal() {
+ observerCount--;
+ if (observerCount == 0) {
+ choreographer.removeFrameCallback(this);
+ sampledVsyncTimeNs = 0;
+ }
+ }
+ }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/VideoSurfaceView.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/VideoSurfaceView.java
new file mode 100644
index 0000000..2478b97
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/VideoSurfaceView.java
@@ -0,0 +1,108 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.media;
+
+import static dev.cobalt.media.Log.TAG;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import dev.cobalt.util.Log;
+
+/**
+ * A Surface view to be used by the video decoder. It informs the Starboard application when the
+ * surface is available so that the decoder can get a reference to it.
+ */
+public class VideoSurfaceView extends SurfaceView {
+
+ public static native void nativeOnLayoutNeeded();
+
+ public static native void nativeOnLayoutScheduled();
+
+ public static native void nativeOnGlobalLayout();
+
+ private Rect videoBounds;
+
+ public VideoSurfaceView(Context context) {
+ super(context);
+ initialize(context);
+ }
+
+ public VideoSurfaceView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initialize(context);
+ }
+
+ public VideoSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initialize(context);
+ }
+
+ public VideoSurfaceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ initialize(context);
+ }
+
+ private void initialize(Context context) {
+ videoBounds = new Rect();
+ setBackgroundColor(Color.TRANSPARENT);
+ getHolder().addCallback(new SurfaceHolderCallback());
+
+ // TODO: Avoid recreating the surface when the player bounds change.
+ // Recreating the surface is time-consuming and complicates synchronizing
+ // punch-out video when the position / size is animated.
+ }
+
+ public boolean updateVideoBounds(final int x, final int y, final int width, final int height) {
+ if (videoBounds.left != x
+ || videoBounds.top != y
+ || videoBounds.right != x + width
+ || videoBounds.bottom != y + height) {
+ videoBounds.set(x, y, x + width, y + height);
+ return true;
+ }
+ return false;
+ }
+
+ private native void nativeOnVideoSurfaceChanged(Surface surface);
+
+ private class SurfaceHolderCallback implements SurfaceHolder.Callback {
+
+ boolean sawInitialChange = false;
+
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ nativeOnVideoSurfaceChanged(holder.getSurface());
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+ // We should only ever see the initial change after creation.
+ if (sawInitialChange) {
+ Log.e(TAG, "Video surface changed; decoding may break");
+ }
+ sawInitialChange = true;
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ nativeOnVideoSurfaceChanged(null);
+ }
+ }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/storage/CobaltStorageLoader.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/storage/CobaltStorageLoader.java
new file mode 100644
index 0000000..315e679
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/storage/CobaltStorageLoader.java
@@ -0,0 +1,124 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.storage;
+
+import static dev.cobalt.util.Log.TAG;
+
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import com.google.protobuf.InvalidProtocolBufferException;
+import dev.cobalt.storage.StorageProto.Storage;
+import dev.cobalt.util.Log;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.Arrays;
+
+/** A class to load Cobalt storage from the file system. */
+public class CobaltStorageLoader {
+ private final File filesDir;
+
+ private static final String STORAGE_NAME_PREFIX = ".starboard";
+ private static final String STORAGE_NAME_SUFFIX = ".storage";
+ private static final String STORAGE_HEADER = "SAV1";
+
+ /**
+ * Initializes the loader with the files directory root.
+ *
+ * @param filesDir a File object representing the files directory.
+ */
+ public CobaltStorageLoader(File filesDir) {
+ if (filesDir == null) {
+ throw new IllegalArgumentException("A valid filesDir object is required");
+ }
+ this.filesDir = filesDir;
+ }
+
+ /**
+ * Reads synchronously the Cobalt storage from the file system.
+ *
+ * @return a snapshot of the Cobalt storage as a proto message.
+ */
+ public Storage loadStorageSnapshot() {
+ String fileName = getStorageFileName();
+ if (fileName == null) {
+ return Storage.getDefaultInstance();
+ }
+ byte[] storageBlob = getStorageBlob(fileName);
+ if (storageBlob == null) {
+ Log.e(TAG, "Failed to get storage blob");
+ return Storage.getDefaultInstance();
+ }
+ if (!validateStorageBlob(storageBlob)) {
+ Log.e(TAG, "Invalid storage blob");
+ return Storage.getDefaultInstance();
+ }
+ storageBlob = Arrays.copyOfRange(storageBlob, STORAGE_HEADER.length(), storageBlob.length);
+ try {
+ return Storage.parseFrom(storageBlob);
+ } catch (InvalidProtocolBufferException e) {
+ Log.e(TAG, "Failed parsing the blob", e);
+ }
+ return Storage.getDefaultInstance();
+ }
+
+ private boolean validateStorageBlob(byte[] storageBlob) {
+ if (storageBlob == null || storageBlob.length < STORAGE_HEADER.length()) {
+ return false;
+ }
+ String header = new String(storageBlob, 0, STORAGE_HEADER.length());
+ return header.equals(STORAGE_HEADER);
+ }
+
+ @Nullable
+ private byte[] getStorageBlob(String fileName) {
+ if (TextUtils.isEmpty(fileName)) {
+ Log.e(TAG, "Invalid empty file name");
+ return null;
+ }
+ try (FileInputStream in = new FileInputStream(new File(filesDir, fileName));
+ ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+ byte[] buffer = new byte[8192];
+ int len;
+ while ((len = in.read(buffer)) != -1) {
+ out.write(buffer, 0, len);
+ }
+ return out.toByteArray();
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to read storage blob", e);
+ }
+ return null;
+ }
+
+ @Nullable
+ private String getStorageFileName() {
+ String[] fileNames = filesDir.list();
+ if (fileNames == null) {
+ Log.w(TAG, "Empty file list");
+ return null;
+ }
+ for (String fileName : fileNames) {
+ if (fileName.startsWith(STORAGE_NAME_PREFIX) && fileName.endsWith(STORAGE_NAME_SUFFIX)) {
+ File storageFile = new File(filesDir, fileName);
+ if (storageFile.length() > 0) {
+ return fileName;
+ }
+ }
+ }
+ Log.w(TAG, "Failed to find storage file name");
+ return null;
+ }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/storage/README.md b/src/starboard/android/apk/app/src/main/java/dev/cobalt/storage/README.md
new file mode 100644
index 0000000..7938fc5
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/storage/README.md
@@ -0,0 +1,15 @@
+The StorageProto java class was generated using the protoc
+compiler from the third_party/protobuf package.
+
+Example:
+ out/linux-x64x11_debug/protoc cobalt/storage/store/storage.proto \
+ --java_out=starboard/android/apk/app/src/main/java
+
+The code doesn't compile cleanly and produces warning:
+ warning: [unchecked] unchecked conversion
+
+To suppress the warning the following annotation should be added
+to the StorageProto class:
+
+@SuppressWarnings("unchecked")
+
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/storage/StorageProto.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/storage/StorageProto.java
new file mode 100644
index 0000000..a07b35d
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/storage/StorageProto.java
@@ -0,0 +1,3591 @@
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: cobalt/storage/store/storage.proto
+
+package dev.cobalt.storage;
+
+@SuppressWarnings("unchecked")
+public final class StorageProto {
+ private StorageProto() {}
+ public static void registerAllExtensions(
+ com.google.protobuf.ExtensionRegistryLite registry) {
+ }
+ public interface CookieOrBuilder extends
+ // @@protoc_insertion_point(interface_extends:cobalt.storage.Cookie)
+ com.google.protobuf.MessageLiteOrBuilder {
+
+ /**
+ * <pre>
+ * The name of the cookie.
+ * </pre>
+ *
+ * <code>optional string name = 1;</code>
+ */
+ java.lang.String getName();
+ /**
+ * <pre>
+ * The name of the cookie.
+ * </pre>
+ *
+ * <code>optional string name = 1;</code>
+ */
+ com.google.protobuf.ByteString
+ getNameBytes();
+
+ /**
+ * <pre>
+ * The value of the cookie.
+ * </pre>
+ *
+ * <code>optional string value = 2;</code>
+ */
+ java.lang.String getValue();
+ /**
+ * <pre>
+ * The value of the cookie.
+ * </pre>
+ *
+ * <code>optional string value = 2;</code>
+ */
+ com.google.protobuf.ByteString
+ getValueBytes();
+
+ /**
+ * <pre>
+ * The domain of the url for which the cookie is store.
+ * </pre>
+ *
+ * <code>optional string domain = 3;</code>
+ */
+ java.lang.String getDomain();
+ /**
+ * <pre>
+ * The domain of the url for which the cookie is store.
+ * </pre>
+ *
+ * <code>optional string domain = 3;</code>
+ */
+ com.google.protobuf.ByteString
+ getDomainBytes();
+
+ /**
+ * <pre>
+ * The path of the url for which the cookie is stored.
+ * </pre>
+ *
+ * <code>optional string path = 4;</code>
+ */
+ java.lang.String getPath();
+ /**
+ * <pre>
+ * The path of the url for which the cookie is stored.
+ * </pre>
+ *
+ * <code>optional string path = 4;</code>
+ */
+ com.google.protobuf.ByteString
+ getPathBytes();
+
+ /**
+ * <pre>
+ * The creation time for the cookie in microseconds since
+ * Windows epoch - 1/1/1601 UTC.
+ * </pre>
+ *
+ * <code>optional int64 creation_time_us = 5;</code>
+ */
+ long getCreationTimeUs();
+
+ /**
+ * <pre>
+ * The expiration time for the cookie in microseconds since
+ * Windows epoch - 1/1/1601 UTC.
+ * </pre>
+ *
+ * <code>optional int64 expiration_time_us = 6;</code>
+ */
+ long getExpirationTimeUs();
+
+ /**
+ * <pre>
+ * The last access time in microseconds since
+ * Windows epoch - 1/1/1601 UTC.
+ * </pre>
+ *
+ * <code>optional int64 last_access_time_us = 7;</code>
+ */
+ long getLastAccessTimeUs();
+
+ /**
+ * <pre>
+ * Whether the cookie should be transmitted only over secure connection.
+ * Defaults to false.
+ * </pre>
+ *
+ * <code>optional bool secure = 8;</code>
+ */
+ boolean getSecure();
+
+ /**
+ * <pre>
+ * Whether this is an HTTP-only cookie. Defaults to false.
+ * </pre>
+ *
+ * <code>optional bool http_only = 9;</code>
+ */
+ boolean getHttpOnly();
+ }
+ /**
+ * <pre>
+ * A single cookie representation.
+ * </pre>
+ *
+ * Protobuf type {@code cobalt.storage.Cookie}
+ */
+ public static final class Cookie extends
+ com.google.protobuf.GeneratedMessageLite<
+ Cookie, Cookie.Builder> implements
+ // @@protoc_insertion_point(message_implements:cobalt.storage.Cookie)
+ CookieOrBuilder {
+ private Cookie() {
+ name_ = "";
+ value_ = "";
+ domain_ = "";
+ path_ = "";
+ creationTimeUs_ = 0L;
+ expirationTimeUs_ = 0L;
+ lastAccessTimeUs_ = 0L;
+ secure_ = false;
+ httpOnly_ = false;
+ }
+ public static final int NAME_FIELD_NUMBER = 1;
+ private java.lang.String name_;
+ /**
+ * <pre>
+ * The name of the cookie.
+ * </pre>
+ *
+ * <code>optional string name = 1;</code>
+ */
+ public java.lang.String getName() {
+ return name_;
+ }
+ /**
+ * <pre>
+ * The name of the cookie.
+ * </pre>
+ *
+ * <code>optional string name = 1;</code>
+ */
+ public com.google.protobuf.ByteString
+ getNameBytes() {
+ return com.google.protobuf.ByteString.copyFromUtf8(name_);
+ }
+ /**
+ * <pre>
+ * The name of the cookie.
+ * </pre>
+ *
+ * <code>optional string name = 1;</code>
+ */
+ private void setName(
+ java.lang.String value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+
+ name_ = value;
+ }
+ /**
+ * <pre>
+ * The name of the cookie.
+ * </pre>
+ *
+ * <code>optional string name = 1;</code>
+ */
+ private void clearName() {
+
+ name_ = getDefaultInstance().getName();
+ }
+ /**
+ * <pre>
+ * The name of the cookie.
+ * </pre>
+ *
+ * <code>optional string name = 1;</code>
+ */
+ private void setNameBytes(
+ com.google.protobuf.ByteString value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ checkByteStringIsUtf8(value);
+
+ name_ = value.toStringUtf8();
+ }
+
+ public static final int VALUE_FIELD_NUMBER = 2;
+ private java.lang.String value_;
+ /**
+ * <pre>
+ * The value of the cookie.
+ * </pre>
+ *
+ * <code>optional string value = 2;</code>
+ */
+ public java.lang.String getValue() {
+ return value_;
+ }
+ /**
+ * <pre>
+ * The value of the cookie.
+ * </pre>
+ *
+ * <code>optional string value = 2;</code>
+ */
+ public com.google.protobuf.ByteString
+ getValueBytes() {
+ return com.google.protobuf.ByteString.copyFromUtf8(value_);
+ }
+ /**
+ * <pre>
+ * The value of the cookie.
+ * </pre>
+ *
+ * <code>optional string value = 2;</code>
+ */
+ private void setValue(
+ java.lang.String value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+
+ value_ = value;
+ }
+ /**
+ * <pre>
+ * The value of the cookie.
+ * </pre>
+ *
+ * <code>optional string value = 2;</code>
+ */
+ private void clearValue() {
+
+ value_ = getDefaultInstance().getValue();
+ }
+ /**
+ * <pre>
+ * The value of the cookie.
+ * </pre>
+ *
+ * <code>optional string value = 2;</code>
+ */
+ private void setValueBytes(
+ com.google.protobuf.ByteString value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ checkByteStringIsUtf8(value);
+
+ value_ = value.toStringUtf8();
+ }
+
+ public static final int DOMAIN_FIELD_NUMBER = 3;
+ private java.lang.String domain_;
+ /**
+ * <pre>
+ * The domain of the url for which the cookie is store.
+ * </pre>
+ *
+ * <code>optional string domain = 3;</code>
+ */
+ public java.lang.String getDomain() {
+ return domain_;
+ }
+ /**
+ * <pre>
+ * The domain of the url for which the cookie is store.
+ * </pre>
+ *
+ * <code>optional string domain = 3;</code>
+ */
+ public com.google.protobuf.ByteString
+ getDomainBytes() {
+ return com.google.protobuf.ByteString.copyFromUtf8(domain_);
+ }
+ /**
+ * <pre>
+ * The domain of the url for which the cookie is store.
+ * </pre>
+ *
+ * <code>optional string domain = 3;</code>
+ */
+ private void setDomain(
+ java.lang.String value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+
+ domain_ = value;
+ }
+ /**
+ * <pre>
+ * The domain of the url for which the cookie is store.
+ * </pre>
+ *
+ * <code>optional string domain = 3;</code>
+ */
+ private void clearDomain() {
+
+ domain_ = getDefaultInstance().getDomain();
+ }
+ /**
+ * <pre>
+ * The domain of the url for which the cookie is store.
+ * </pre>
+ *
+ * <code>optional string domain = 3;</code>
+ */
+ private void setDomainBytes(
+ com.google.protobuf.ByteString value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ checkByteStringIsUtf8(value);
+
+ domain_ = value.toStringUtf8();
+ }
+
+ public static final int PATH_FIELD_NUMBER = 4;
+ private java.lang.String path_;
+ /**
+ * <pre>
+ * The path of the url for which the cookie is stored.
+ * </pre>
+ *
+ * <code>optional string path = 4;</code>
+ */
+ public java.lang.String getPath() {
+ return path_;
+ }
+ /**
+ * <pre>
+ * The path of the url for which the cookie is stored.
+ * </pre>
+ *
+ * <code>optional string path = 4;</code>
+ */
+ public com.google.protobuf.ByteString
+ getPathBytes() {
+ return com.google.protobuf.ByteString.copyFromUtf8(path_);
+ }
+ /**
+ * <pre>
+ * The path of the url for which the cookie is stored.
+ * </pre>
+ *
+ * <code>optional string path = 4;</code>
+ */
+ private void setPath(
+ java.lang.String value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+
+ path_ = value;
+ }
+ /**
+ * <pre>
+ * The path of the url for which the cookie is stored.
+ * </pre>
+ *
+ * <code>optional string path = 4;</code>
+ */
+ private void clearPath() {
+
+ path_ = getDefaultInstance().getPath();
+ }
+ /**
+ * <pre>
+ * The path of the url for which the cookie is stored.
+ * </pre>
+ *
+ * <code>optional string path = 4;</code>
+ */
+ private void setPathBytes(
+ com.google.protobuf.ByteString value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ checkByteStringIsUtf8(value);
+
+ path_ = value.toStringUtf8();
+ }
+
+ public static final int CREATION_TIME_US_FIELD_NUMBER = 5;
+ private long creationTimeUs_;
+ /**
+ * <pre>
+ * The creation time for the cookie in microseconds since
+ * Windows epoch - 1/1/1601 UTC.
+ * </pre>
+ *
+ * <code>optional int64 creation_time_us = 5;</code>
+ */
+ public long getCreationTimeUs() {
+ return creationTimeUs_;
+ }
+ /**
+ * <pre>
+ * The creation time for the cookie in microseconds since
+ * Windows epoch - 1/1/1601 UTC.
+ * </pre>
+ *
+ * <code>optional int64 creation_time_us = 5;</code>
+ */
+ private void setCreationTimeUs(long value) {
+
+ creationTimeUs_ = value;
+ }
+ /**
+ * <pre>
+ * The creation time for the cookie in microseconds since
+ * Windows epoch - 1/1/1601 UTC.
+ * </pre>
+ *
+ * <code>optional int64 creation_time_us = 5;</code>
+ */
+ private void clearCreationTimeUs() {
+
+ creationTimeUs_ = 0L;
+ }
+
+ public static final int EXPIRATION_TIME_US_FIELD_NUMBER = 6;
+ private long expirationTimeUs_;
+ /**
+ * <pre>
+ * The expiration time for the cookie in microseconds since
+ * Windows epoch - 1/1/1601 UTC.
+ * </pre>
+ *
+ * <code>optional int64 expiration_time_us = 6;</code>
+ */
+ public long getExpirationTimeUs() {
+ return expirationTimeUs_;
+ }
+ /**
+ * <pre>
+ * The expiration time for the cookie in microseconds since
+ * Windows epoch - 1/1/1601 UTC.
+ * </pre>
+ *
+ * <code>optional int64 expiration_time_us = 6;</code>
+ */
+ private void setExpirationTimeUs(long value) {
+
+ expirationTimeUs_ = value;
+ }
+ /**
+ * <pre>
+ * The expiration time for the cookie in microseconds since
+ * Windows epoch - 1/1/1601 UTC.
+ * </pre>
+ *
+ * <code>optional int64 expiration_time_us = 6;</code>
+ */
+ private void clearExpirationTimeUs() {
+
+ expirationTimeUs_ = 0L;
+ }
+
+ public static final int LAST_ACCESS_TIME_US_FIELD_NUMBER = 7;
+ private long lastAccessTimeUs_;
+ /**
+ * <pre>
+ * The last access time in microseconds since
+ * Windows epoch - 1/1/1601 UTC.
+ * </pre>
+ *
+ * <code>optional int64 last_access_time_us = 7;</code>
+ */
+ public long getLastAccessTimeUs() {
+ return lastAccessTimeUs_;
+ }
+ /**
+ * <pre>
+ * The last access time in microseconds since
+ * Windows epoch - 1/1/1601 UTC.
+ * </pre>
+ *
+ * <code>optional int64 last_access_time_us = 7;</code>
+ */
+ private void setLastAccessTimeUs(long value) {
+
+ lastAccessTimeUs_ = value;
+ }
+ /**
+ * <pre>
+ * The last access time in microseconds since
+ * Windows epoch - 1/1/1601 UTC.
+ * </pre>
+ *
+ * <code>optional int64 last_access_time_us = 7;</code>
+ */
+ private void clearLastAccessTimeUs() {
+
+ lastAccessTimeUs_ = 0L;
+ }
+
+ public static final int SECURE_FIELD_NUMBER = 8;
+ private boolean secure_;
+ /**
+ * <pre>
+ * Whether the cookie should be transmitted only over secure connection.
+ * Defaults to false.
+ * </pre>
+ *
+ * <code>optional bool secure = 8;</code>
+ */
+ public boolean getSecure() {
+ return secure_;
+ }
+ /**
+ * <pre>
+ * Whether the cookie should be transmitted only over secure connection.
+ * Defaults to false.
+ * </pre>
+ *
+ * <code>optional bool secure = 8;</code>
+ */
+ private void setSecure(boolean value) {
+
+ secure_ = value;
+ }
+ /**
+ * <pre>
+ * Whether the cookie should be transmitted only over secure connection.
+ * Defaults to false.
+ * </pre>
+ *
+ * <code>optional bool secure = 8;</code>
+ */
+ private void clearSecure() {
+
+ secure_ = false;
+ }
+
+ public static final int HTTP_ONLY_FIELD_NUMBER = 9;
+ private boolean httpOnly_;
+ /**
+ * <pre>
+ * Whether this is an HTTP-only cookie. Defaults to false.
+ * </pre>
+ *
+ * <code>optional bool http_only = 9;</code>
+ */
+ public boolean getHttpOnly() {
+ return httpOnly_;
+ }
+ /**
+ * <pre>
+ * Whether this is an HTTP-only cookie. Defaults to false.
+ * </pre>
+ *
+ * <code>optional bool http_only = 9;</code>
+ */
+ private void setHttpOnly(boolean value) {
+
+ httpOnly_ = value;
+ }
+ /**
+ * <pre>
+ * Whether this is an HTTP-only cookie. Defaults to false.
+ * </pre>
+ *
+ * <code>optional bool http_only = 9;</code>
+ */
+ private void clearHttpOnly() {
+
+ httpOnly_ = false;
+ }
+
+ public void writeTo(com.google.protobuf.CodedOutputStream output)
+ throws java.io.IOException {
+ if (!name_.isEmpty()) {
+ output.writeString(1, getName());
+ }
+ if (!value_.isEmpty()) {
+ output.writeString(2, getValue());
+ }
+ if (!domain_.isEmpty()) {
+ output.writeString(3, getDomain());
+ }
+ if (!path_.isEmpty()) {
+ output.writeString(4, getPath());
+ }
+ if (creationTimeUs_ != 0L) {
+ output.writeInt64(5, creationTimeUs_);
+ }
+ if (expirationTimeUs_ != 0L) {
+ output.writeInt64(6, expirationTimeUs_);
+ }
+ if (lastAccessTimeUs_ != 0L) {
+ output.writeInt64(7, lastAccessTimeUs_);
+ }
+ if (secure_ != false) {
+ output.writeBool(8, secure_);
+ }
+ if (httpOnly_ != false) {
+ output.writeBool(9, httpOnly_);
+ }
+ }
+
+ public int getSerializedSize() {
+ int size = memoizedSerializedSize;
+ if (size != -1) return size;
+
+ size = 0;
+ if (!name_.isEmpty()) {
+ size += com.google.protobuf.CodedOutputStream
+ .computeStringSize(1, getName());
+ }
+ if (!value_.isEmpty()) {
+ size += com.google.protobuf.CodedOutputStream
+ .computeStringSize(2, getValue());
+ }
+ if (!domain_.isEmpty()) {
+ size += com.google.protobuf.CodedOutputStream
+ .computeStringSize(3, getDomain());
+ }
+ if (!path_.isEmpty()) {
+ size += com.google.protobuf.CodedOutputStream
+ .computeStringSize(4, getPath());
+ }
+ if (creationTimeUs_ != 0L) {
+ size += com.google.protobuf.CodedOutputStream
+ .computeInt64Size(5, creationTimeUs_);
+ }
+ if (expirationTimeUs_ != 0L) {
+ size += com.google.protobuf.CodedOutputStream
+ .computeInt64Size(6, expirationTimeUs_);
+ }
+ if (lastAccessTimeUs_ != 0L) {
+ size += com.google.protobuf.CodedOutputStream
+ .computeInt64Size(7, lastAccessTimeUs_);
+ }
+ if (secure_ != false) {
+ size += com.google.protobuf.CodedOutputStream
+ .computeBoolSize(8, secure_);
+ }
+ if (httpOnly_ != false) {
+ size += com.google.protobuf.CodedOutputStream
+ .computeBoolSize(9, httpOnly_);
+ }
+ memoizedSerializedSize = size;
+ return size;
+ }
+
+ public static dev.cobalt.storage.StorageProto.Cookie parseFrom(
+ com.google.protobuf.ByteString data)
+ throws com.google.protobuf.InvalidProtocolBufferException {
+ return com.google.protobuf.GeneratedMessageLite.parseFrom(
+ DEFAULT_INSTANCE, data);
+ }
+ public static dev.cobalt.storage.StorageProto.Cookie parseFrom(
+ com.google.protobuf.ByteString data,
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws com.google.protobuf.InvalidProtocolBufferException {
+ return com.google.protobuf.GeneratedMessageLite.parseFrom(
+ DEFAULT_INSTANCE, data, extensionRegistry);
+ }
+ public static dev.cobalt.storage.StorageProto.Cookie parseFrom(byte[] data)
+ throws com.google.protobuf.InvalidProtocolBufferException {
+ return com.google.protobuf.GeneratedMessageLite.parseFrom(
+ DEFAULT_INSTANCE, data);
+ }
+ public static dev.cobalt.storage.StorageProto.Cookie parseFrom(
+ byte[] data,
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws com.google.protobuf.InvalidProtocolBufferException {
+ return com.google.protobuf.GeneratedMessageLite.parseFrom(
+ DEFAULT_INSTANCE, data, extensionRegistry);
+ }
+ public static dev.cobalt.storage.StorageProto.Cookie parseFrom(java.io.InputStream input)
+ throws java.io.IOException {
+ return com.google.protobuf.GeneratedMessageLite.parseFrom(
+ DEFAULT_INSTANCE, input);
+ }
+ public static dev.cobalt.storage.StorageProto.Cookie parseFrom(
+ java.io.InputStream input,
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws java.io.IOException {
+ return com.google.protobuf.GeneratedMessageLite.parseFrom(
+ DEFAULT_INSTANCE, input, extensionRegistry);
+ }
+ public static dev.cobalt.storage.StorageProto.Cookie parseDelimitedFrom(java.io.InputStream input)
+ throws java.io.IOException {
+ return parseDelimitedFrom(DEFAULT_INSTANCE, input);
+ }
+ public static dev.cobalt.storage.StorageProto.Cookie parseDelimitedFrom(
+ java.io.InputStream input,
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws java.io.IOException {
+ return parseDelimitedFrom(DEFAULT_INSTANCE, input, extensionRegistry);
+ }
+ public static dev.cobalt.storage.StorageProto.Cookie parseFrom(
+ com.google.protobuf.CodedInputStream input)
+ throws java.io.IOException {
+ return com.google.protobuf.GeneratedMessageLite.parseFrom(
+ DEFAULT_INSTANCE, input);
+ }
+ public static dev.cobalt.storage.StorageProto.Cookie parseFrom(
+ com.google.protobuf.CodedInputStream input,
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws java.io.IOException {
+ return com.google.protobuf.GeneratedMessageLite.parseFrom(
+ DEFAULT_INSTANCE, input, extensionRegistry);
+ }
+
+ public static Builder newBuilder() {
+ return DEFAULT_INSTANCE.toBuilder();
+ }
+ public static Builder newBuilder(dev.cobalt.storage.StorageProto.Cookie prototype) {
+ return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype);
+ }
+
+ /**
+ * <pre>
+ * A single cookie representation.
+ * </pre>
+ *
+ * Protobuf type {@code cobalt.storage.Cookie}
+ */
+ public static final class Builder extends
+ com.google.protobuf.GeneratedMessageLite.Builder<
+ dev.cobalt.storage.StorageProto.Cookie, Builder> implements
+ // @@protoc_insertion_point(builder_implements:cobalt.storage.Cookie)
+ dev.cobalt.storage.StorageProto.CookieOrBuilder {
+ // Construct using dev.cobalt.storage.StorageProto.Cookie.newBuilder()
+ private Builder() {
+ super(DEFAULT_INSTANCE);
+ }
+
+
+ /**
+ * <pre>
+ * The name of the cookie.
+ * </pre>
+ *
+ * <code>optional string name = 1;</code>
+ */
+ public java.lang.String getName() {
+ return instance.getName();
+ }
+ /**
+ * <pre>
+ * The name of the cookie.
+ * </pre>
+ *
+ * <code>optional string name = 1;</code>
+ */
+ public com.google.protobuf.ByteString
+ getNameBytes() {
+ return instance.getNameBytes();
+ }
+ /**
+ * <pre>
+ * The name of the cookie.
+ * </pre>
+ *
+ * <code>optional string name = 1;</code>
+ */
+ public Builder setName(
+ java.lang.String value) {
+ copyOnWrite();
+ instance.setName(value);
+ return this;
+ }
+ /**
+ * <pre>
+ * The name of the cookie.
+ * </pre>
+ *
+ * <code>optional string name = 1;</code>
+ */
+ public Builder clearName() {
+ copyOnWrite();
+ instance.clearName();
+ return this;
+ }
+ /**
+ * <pre>
+ * The name of the cookie.
+ * </pre>
+ *
+ * <code>optional string name = 1;</code>
+ */
+ public Builder setNameBytes(
+ com.google.protobuf.ByteString value) {
+ copyOnWrite();
+ instance.setNameBytes(value);
+ return this;
+ }
+
+ /**
+ * <pre>
+ * The value of the cookie.
+ * </pre>
+ *
+ * <code>optional string value = 2;</code>
+ */
+ public java.lang.String getValue() {
+ return instance.getValue();
+ }
+ /**
+ * <pre>
+ * The value of the cookie.
+ * </pre>
+ *
+ * <code>optional string value = 2;</code>
+ */
+ public com.google.protobuf.ByteString
+ getValueBytes() {
+ return instance.getValueBytes();
+ }
+ /**
+ * <pre>
+ * The value of the cookie.
+ * </pre>
+ *
+ * <code>optional string value = 2;</code>
+ */
+ public Builder setValue(
+ java.lang.String value) {
+ copyOnWrite();
+ instance.setValue(value);
+ return this;
+ }
+ /**
+ * <pre>
+ * The value of the cookie.
+ * </pre>
+ *
+ * <code>optional string value = 2;</code>
+ */
+ public Builder clearValue() {
+ copyOnWrite();
+ instance.clearValue();
+ return this;
+ }
+ /**
+ * <pre>
+ * The value of the cookie.
+ * </pre>
+ *
+ * <code>optional string value = 2;</code>
+ */
+ public Builder setValueBytes(
+ com.google.protobuf.ByteString value) {
+ copyOnWrite();
+ instance.setValueBytes(value);
+ return this;
+ }
+
+ /**
+ * <pre>
+ * The domain of the url for which the cookie is store.
+ * </pre>
+ *
+ * <code>optional string domain = 3;</code>
+ */
+ public java.lang.String getDomain() {
+ return instance.getDomain();
+ }
+ /**
+ * <pre>
+ * The domain of the url for which the cookie is store.
+ * </pre>
+ *
+ * <code>optional string domain = 3;</code>
+ */
+ public com.google.protobuf.ByteString
+ getDomainBytes() {
+ return instance.getDomainBytes();
+ }
+ /**
+ * <pre>
+ * The domain of the url for which the cookie is store.
+ * </pre>
+ *
+ * <code>optional string domain = 3;</code>
+ */
+ public Builder setDomain(
+ java.lang.String value) {
+ copyOnWrite();
+ instance.setDomain(value);
+ return this;
+ }
+ /**
+ * <pre>
+ * The domain of the url for which the cookie is store.
+ * </pre>
+ *
+ * <code>optional string domain = 3;</code>
+ */
+ public Builder clearDomain() {
+ copyOnWrite();
+ instance.clearDomain();
+ return this;
+ }
+ /**
+ * <pre>
+ * The domain of the url for which the cookie is store.
+ * </pre>
+ *
+ * <code>optional string domain = 3;</code>
+ */
+ public Builder setDomainBytes(
+ com.google.protobuf.ByteString value) {
+ copyOnWrite();
+ instance.setDomainBytes(value);
+ return this;
+ }
+
+ /**
+ * <pre>
+ * The path of the url for which the cookie is stored.
+ * </pre>
+ *
+ * <code>optional string path = 4;</code>
+ */
+ public java.lang.String getPath() {
+ return instance.getPath();
+ }
+ /**
+ * <pre>
+ * The path of the url for which the cookie is stored.
+ * </pre>
+ *
+ * <code>optional string path = 4;</code>
+ */
+ public com.google.protobuf.ByteString
+ getPathBytes() {
+ return instance.getPathBytes();
+ }
+ /**
+ * <pre>
+ * The path of the url for which the cookie is stored.
+ * </pre>
+ *
+ * <code>optional string path = 4;</code>
+ */
+ public Builder setPath(
+ java.lang.String value) {
+ copyOnWrite();
+ instance.setPath(value);
+ return this;
+ }
+ /**
+ * <pre>
+ * The path of the url for which the cookie is stored.
+ * </pre>
+ *
+ * <code>optional string path = 4;</code>
+ */
+ public Builder clearPath() {
+ copyOnWrite();
+ instance.clearPath();
+ return this;
+ }
+ /**
+ * <pre>
+ * The path of the url for which the cookie is stored.
+ * </pre>
+ *
+ * <code>optional string path = 4;</code>
+ */
+ public Builder setPathBytes(
+ com.google.protobuf.ByteString value) {
+ copyOnWrite();
+ instance.setPathBytes(value);
+ return this;
+ }
+
+ /**
+ * <pre>
+ * The creation time for the cookie in microseconds since
+ * Windows epoch - 1/1/1601 UTC.
+ * </pre>
+ *
+ * <code>optional int64 creation_time_us = 5;</code>
+ */
+ public long getCreationTimeUs() {
+ return instance.getCreationTimeUs();
+ }
+ /**
+ * <pre>
+ * The creation time for the cookie in microseconds since
+ * Windows epoch - 1/1/1601 UTC.
+ * </pre>
+ *
+ * <code>optional int64 creation_time_us = 5;</code>
+ */
+ public Builder setCreationTimeUs(long value) {
+ copyOnWrite();
+ instance.setCreationTimeUs(value);
+ return this;
+ }
+ /**
+ * <pre>
+ * The creation time for the cookie in microseconds since
+ * Windows epoch - 1/1/1601 UTC.
+ * </pre>
+ *
+ * <code>optional int64 creation_time_us = 5;</code>
+ */
+ public Builder clearCreationTimeUs() {
+ copyOnWrite();
+ instance.clearCreationTimeUs();
+ return this;
+ }
+
+ /**
+ * <pre>
+ * The expiration time for the cookie in microseconds since
+ * Windows epoch - 1/1/1601 UTC.
+ * </pre>
+ *
+ * <code>optional int64 expiration_time_us = 6;</code>
+ */
+ public long getExpirationTimeUs() {
+ return instance.getExpirationTimeUs();
+ }
+ /**
+ * <pre>
+ * The expiration time for the cookie in microseconds since
+ * Windows epoch - 1/1/1601 UTC.
+ * </pre>
+ *
+ * <code>optional int64 expiration_time_us = 6;</code>
+ */
+ public Builder setExpirationTimeUs(long value) {
+ copyOnWrite();
+ instance.setExpirationTimeUs(value);
+ return this;
+ }
+ /**
+ * <pre>
+ * The expiration time for the cookie in microseconds since
+ * Windows epoch - 1/1/1601 UTC.
+ * </pre>
+ *
+ * <code>optional int64 expiration_time_us = 6;</code>
+ */
+ public Builder clearExpirationTimeUs() {
+ copyOnWrite();
+ instance.clearExpirationTimeUs();
+ return this;
+ }
+
+ /**
+ * <pre>
+ * The last access time in microseconds since
+ * Windows epoch - 1/1/1601 UTC.
+ * </pre>
+ *
+ * <code>optional int64 last_access_time_us = 7;</code>
+ */
+ public long getLastAccessTimeUs() {
+ return instance.getLastAccessTimeUs();
+ }
+ /**
+ * <pre>
+ * The last access time in microseconds since
+ * Windows epoch - 1/1/1601 UTC.
+ * </pre>
+ *
+ * <code>optional int64 last_access_time_us = 7;</code>
+ */
+ public Builder setLastAccessTimeUs(long value) {
+ copyOnWrite();
+ instance.setLastAccessTimeUs(value);
+ return this;
+ }
+ /**
+ * <pre>
+ * The last access time in microseconds since
+ * Windows epoch - 1/1/1601 UTC.
+ * </pre>
+ *
+ * <code>optional int64 last_access_time_us = 7;</code>
+ */
+ public Builder clearLastAccessTimeUs() {
+ copyOnWrite();
+ instance.clearLastAccessTimeUs();
+ return this;
+ }
+
+ /**
+ * <pre>
+ * Whether the cookie should be transmitted only over secure connection.
+ * Defaults to false.
+ * </pre>
+ *
+ * <code>optional bool secure = 8;</code>
+ */
+ public boolean getSecure() {
+ return instance.getSecure();
+ }
+ /**
+ * <pre>
+ * Whether the cookie should be transmitted only over secure connection.
+ * Defaults to false.
+ * </pre>
+ *
+ * <code>optional bool secure = 8;</code>
+ */
+ public Builder setSecure(boolean value) {
+ copyOnWrite();
+ instance.setSecure(value);
+ return this;
+ }
+ /**
+ * <pre>
+ * Whether the cookie should be transmitted only over secure connection.
+ * Defaults to false.
+ * </pre>
+ *
+ * <code>optional bool secure = 8;</code>
+ */
+ public Builder clearSecure() {
+ copyOnWrite();
+ instance.clearSecure();
+ return this;
+ }
+
+ /**
+ * <pre>
+ * Whether this is an HTTP-only cookie. Defaults to false.
+ * </pre>
+ *
+ * <code>optional bool http_only = 9;</code>
+ */
+ public boolean getHttpOnly() {
+ return instance.getHttpOnly();
+ }
+ /**
+ * <pre>
+ * Whether this is an HTTP-only cookie. Defaults to false.
+ * </pre>
+ *
+ * <code>optional bool http_only = 9;</code>
+ */
+ public Builder setHttpOnly(boolean value) {
+ copyOnWrite();
+ instance.setHttpOnly(value);
+ return this;
+ }
+ /**
+ * <pre>
+ * Whether this is an HTTP-only cookie. Defaults to false.
+ * </pre>
+ *
+ * <code>optional bool http_only = 9;</code>
+ */
+ public Builder clearHttpOnly() {
+ copyOnWrite();
+ instance.clearHttpOnly();
+ return this;
+ }
+
+ // @@protoc_insertion_point(builder_scope:cobalt.storage.Cookie)
+ }
+ protected final Object dynamicMethod(
+ com.google.protobuf.GeneratedMessageLite.MethodToInvoke method,
+ Object arg0, Object arg1) {
+ switch (method) {
+ case NEW_MUTABLE_INSTANCE: {
+ return new dev.cobalt.storage.StorageProto.Cookie();
+ }
+ case IS_INITIALIZED: {
+ return DEFAULT_INSTANCE;
+ }
+ case MAKE_IMMUTABLE: {
+ return null;
+ }
+ case NEW_BUILDER: {
+ return new Builder();
+ }
+ case VISIT: {
+ Visitor visitor = (Visitor) arg0;
+ dev.cobalt.storage.StorageProto.Cookie other = (dev.cobalt.storage.StorageProto.Cookie) arg1;
+ name_ = visitor.visitString(!name_.isEmpty(), name_,
+ !other.name_.isEmpty(), other.name_);
+ value_ = visitor.visitString(!value_.isEmpty(), value_,
+ !other.value_.isEmpty(), other.value_);
+ domain_ = visitor.visitString(!domain_.isEmpty(), domain_,
+ !other.domain_.isEmpty(), other.domain_);
+ path_ = visitor.visitString(!path_.isEmpty(), path_,
+ !other.path_.isEmpty(), other.path_);
+ creationTimeUs_ = visitor.visitLong(creationTimeUs_ != 0L, creationTimeUs_,
+ other.creationTimeUs_ != 0L, other.creationTimeUs_);
+ expirationTimeUs_ = visitor.visitLong(expirationTimeUs_ != 0L, expirationTimeUs_,
+ other.expirationTimeUs_ != 0L, other.expirationTimeUs_);
+ lastAccessTimeUs_ = visitor.visitLong(lastAccessTimeUs_ != 0L, lastAccessTimeUs_,
+ other.lastAccessTimeUs_ != 0L, other.lastAccessTimeUs_);
+ secure_ = visitor.visitBoolean(secure_ != false, secure_,
+ other.secure_ != false, other.secure_);
+ httpOnly_ = visitor.visitBoolean(httpOnly_ != false, httpOnly_,
+ other.httpOnly_ != false, other.httpOnly_);
+ if (visitor == com.google.protobuf.GeneratedMessageLite.MergeFromVisitor
+ .INSTANCE) {
+ }
+ return this;
+ }
+ case MERGE_FROM_STREAM: {
+ com.google.protobuf.CodedInputStream input =
+ (com.google.protobuf.CodedInputStream) arg0;
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry =
+ (com.google.protobuf.ExtensionRegistryLite) arg1;
+ try {
+ boolean done = false;
+ while (!done) {
+ int tag = input.readTag();
+ switch (tag) {
+ case 0:
+ done = true;
+ break;
+ default: {
+ if (!input.skipField(tag)) {
+ done = true;
+ }
+ break;
+ }
+ case 10: {
+ String s = input.readStringRequireUtf8();
+
+ name_ = s;
+ break;
+ }
+ case 18: {
+ String s = input.readStringRequireUtf8();
+
+ value_ = s;
+ break;
+ }
+ case 26: {
+ String s = input.readStringRequireUtf8();
+
+ domain_ = s;
+ break;
+ }
+ case 34: {
+ String s = input.readStringRequireUtf8();
+
+ path_ = s;
+ break;
+ }
+ case 40: {
+
+ creationTimeUs_ = input.readInt64();
+ break;
+ }
+ case 48: {
+
+ expirationTimeUs_ = input.readInt64();
+ break;
+ }
+ case 56: {
+
+ lastAccessTimeUs_ = input.readInt64();
+ break;
+ }
+ case 64: {
+
+ secure_ = input.readBool();
+ break;
+ }
+ case 72: {
+
+ httpOnly_ = input.readBool();
+ break;
+ }
+ }
+ }
+ } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+ throw new RuntimeException(e.setUnfinishedMessage(this));
+ } catch (java.io.IOException e) {
+ throw new RuntimeException(
+ new com.google.protobuf.InvalidProtocolBufferException(
+ e.getMessage()).setUnfinishedMessage(this));
+ } finally {
+ }
+ }
+ case GET_DEFAULT_INSTANCE: {
+ return DEFAULT_INSTANCE;
+ }
+ case GET_PARSER: {
+ if (PARSER == null) { synchronized (dev.cobalt.storage.StorageProto.Cookie.class) {
+ if (PARSER == null) {
+ PARSER = new DefaultInstanceBasedParser(DEFAULT_INSTANCE);
+ }
+ }
+ }
+ return PARSER;
+ }
+ }
+ throw new UnsupportedOperationException();
+ }
+
+
+ // @@protoc_insertion_point(class_scope:cobalt.storage.Cookie)
+ private static final dev.cobalt.storage.StorageProto.Cookie DEFAULT_INSTANCE;
+ static {
+ DEFAULT_INSTANCE = new Cookie();
+ DEFAULT_INSTANCE.makeImmutable();
+ }
+
+ public static dev.cobalt.storage.StorageProto.Cookie getDefaultInstance() {
+ return DEFAULT_INSTANCE;
+ }
+
+ private static volatile com.google.protobuf.Parser<Cookie> PARSER;
+
+ public static com.google.protobuf.Parser<Cookie> parser() {
+ return DEFAULT_INSTANCE.getParserForType();
+ }
+ }
+
+ public interface LocalStorageEntryOrBuilder extends
+ // @@protoc_insertion_point(interface_extends:cobalt.storage.LocalStorageEntry)
+ com.google.protobuf.MessageLiteOrBuilder {
+
+ /**
+ * <pre>
+ * The key for the local storage entry.
+ * </pre>
+ *
+ * <code>optional string key = 1;</code>
+ */
+ java.lang.String getKey();
+ /**
+ * <pre>
+ * The key for the local storage entry.
+ * </pre>
+ *
+ * <code>optional string key = 1;</code>
+ */
+ com.google.protobuf.ByteString
+ getKeyBytes();
+
+ /**
+ * <pre>
+ * The value of the local storage entry.
+ * </pre>
+ *
+ * <code>optional string value = 2;</code>
+ */
+ java.lang.String getValue();
+ /**
+ * <pre>
+ * The value of the local storage entry.
+ * </pre>
+ *
+ * <code>optional string value = 2;</code>
+ */
+ com.google.protobuf.ByteString
+ getValueBytes();
+ }
+ /**
+ * <pre>
+ * A single local storage entry.
+ * </pre>
+ *
+ * Protobuf type {@code cobalt.storage.LocalStorageEntry}
+ */
+ public static final class LocalStorageEntry extends
+ com.google.protobuf.GeneratedMessageLite<
+ LocalStorageEntry, LocalStorageEntry.Builder> implements
+ // @@protoc_insertion_point(message_implements:cobalt.storage.LocalStorageEntry)
+ LocalStorageEntryOrBuilder {
+ private LocalStorageEntry() {
+ key_ = "";
+ value_ = "";
+ }
+ public static final int KEY_FIELD_NUMBER = 1;
+ private java.lang.String key_;
+ /**
+ * <pre>
+ * The key for the local storage entry.
+ * </pre>
+ *
+ * <code>optional string key = 1;</code>
+ */
+ public java.lang.String getKey() {
+ return key_;
+ }
+ /**
+ * <pre>
+ * The key for the local storage entry.
+ * </pre>
+ *
+ * <code>optional string key = 1;</code>
+ */
+ public com.google.protobuf.ByteString
+ getKeyBytes() {
+ return com.google.protobuf.ByteString.copyFromUtf8(key_);
+ }
+ /**
+ * <pre>
+ * The key for the local storage entry.
+ * </pre>
+ *
+ * <code>optional string key = 1;</code>
+ */
+ private void setKey(
+ java.lang.String value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+
+ key_ = value;
+ }
+ /**
+ * <pre>
+ * The key for the local storage entry.
+ * </pre>
+ *
+ * <code>optional string key = 1;</code>
+ */
+ private void clearKey() {
+
+ key_ = getDefaultInstance().getKey();
+ }
+ /**
+ * <pre>
+ * The key for the local storage entry.
+ * </pre>
+ *
+ * <code>optional string key = 1;</code>
+ */
+ private void setKeyBytes(
+ com.google.protobuf.ByteString value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ checkByteStringIsUtf8(value);
+
+ key_ = value.toStringUtf8();
+ }
+
+ public static final int VALUE_FIELD_NUMBER = 2;
+ private java.lang.String value_;
+ /**
+ * <pre>
+ * The value of the local storage entry.
+ * </pre>
+ *
+ * <code>optional string value = 2;</code>
+ */
+ public java.lang.String getValue() {
+ return value_;
+ }
+ /**
+ * <pre>
+ * The value of the local storage entry.
+ * </pre>
+ *
+ * <code>optional string value = 2;</code>
+ */
+ public com.google.protobuf.ByteString
+ getValueBytes() {
+ return com.google.protobuf.ByteString.copyFromUtf8(value_);
+ }
+ /**
+ * <pre>
+ * The value of the local storage entry.
+ * </pre>
+ *
+ * <code>optional string value = 2;</code>
+ */
+ private void setValue(
+ java.lang.String value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+
+ value_ = value;
+ }
+ /**
+ * <pre>
+ * The value of the local storage entry.
+ * </pre>
+ *
+ * <code>optional string value = 2;</code>
+ */
+ private void clearValue() {
+
+ value_ = getDefaultInstance().getValue();
+ }
+ /**
+ * <pre>
+ * The value of the local storage entry.
+ * </pre>
+ *
+ * <code>optional string value = 2;</code>
+ */
+ private void setValueBytes(
+ com.google.protobuf.ByteString value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ checkByteStringIsUtf8(value);
+
+ value_ = value.toStringUtf8();
+ }
+
+ public void writeTo(com.google.protobuf.CodedOutputStream output)
+ throws java.io.IOException {
+ if (!key_.isEmpty()) {
+ output.writeString(1, getKey());
+ }
+ if (!value_.isEmpty()) {
+ output.writeString(2, getValue());
+ }
+ }
+
+ public int getSerializedSize() {
+ int size = memoizedSerializedSize;
+ if (size != -1) return size;
+
+ size = 0;
+ if (!key_.isEmpty()) {
+ size += com.google.protobuf.CodedOutputStream
+ .computeStringSize(1, getKey());
+ }
+ if (!value_.isEmpty()) {
+ size += com.google.protobuf.CodedOutputStream
+ .computeStringSize(2, getValue());
+ }
+ memoizedSerializedSize = size;
+ return size;
+ }
+
+ public static dev.cobalt.storage.StorageProto.LocalStorageEntry parseFrom(
+ com.google.protobuf.ByteString data)
+ throws com.google.protobuf.InvalidProtocolBufferException {
+ return com.google.protobuf.GeneratedMessageLite.parseFrom(
+ DEFAULT_INSTANCE, data);
+ }
+ public static dev.cobalt.storage.StorageProto.LocalStorageEntry parseFrom(
+ com.google.protobuf.ByteString data,
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws com.google.protobuf.InvalidProtocolBufferException {
+ return com.google.protobuf.GeneratedMessageLite.parseFrom(
+ DEFAULT_INSTANCE, data, extensionRegistry);
+ }
+ public static dev.cobalt.storage.StorageProto.LocalStorageEntry parseFrom(byte[] data)
+ throws com.google.protobuf.InvalidProtocolBufferException {
+ return com.google.protobuf.GeneratedMessageLite.parseFrom(
+ DEFAULT_INSTANCE, data);
+ }
+ public static dev.cobalt.storage.StorageProto.LocalStorageEntry parseFrom(
+ byte[] data,
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws com.google.protobuf.InvalidProtocolBufferException {
+ return com.google.protobuf.GeneratedMessageLite.parseFrom(
+ DEFAULT_INSTANCE, data, extensionRegistry);
+ }
+ public static dev.cobalt.storage.StorageProto.LocalStorageEntry parseFrom(java.io.InputStream input)
+ throws java.io.IOException {
+ return com.google.protobuf.GeneratedMessageLite.parseFrom(
+ DEFAULT_INSTANCE, input);
+ }
+ public static dev.cobalt.storage.StorageProto.LocalStorageEntry parseFrom(
+ java.io.InputStream input,
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws java.io.IOException {
+ return com.google.protobuf.GeneratedMessageLite.parseFrom(
+ DEFAULT_INSTANCE, input, extensionRegistry);
+ }
+ public static dev.cobalt.storage.StorageProto.LocalStorageEntry parseDelimitedFrom(java.io.InputStream input)
+ throws java.io.IOException {
+ return parseDelimitedFrom(DEFAULT_INSTANCE, input);
+ }
+ public static dev.cobalt.storage.StorageProto.LocalStorageEntry parseDelimitedFrom(
+ java.io.InputStream input,
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws java.io.IOException {
+ return parseDelimitedFrom(DEFAULT_INSTANCE, input, extensionRegistry);
+ }
+ public static dev.cobalt.storage.StorageProto.LocalStorageEntry parseFrom(
+ com.google.protobuf.CodedInputStream input)
+ throws java.io.IOException {
+ return com.google.protobuf.GeneratedMessageLite.parseFrom(
+ DEFAULT_INSTANCE, input);
+ }
+ public static dev.cobalt.storage.StorageProto.LocalStorageEntry parseFrom(
+ com.google.protobuf.CodedInputStream input,
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws java.io.IOException {
+ return com.google.protobuf.GeneratedMessageLite.parseFrom(
+ DEFAULT_INSTANCE, input, extensionRegistry);
+ }
+
+ public static Builder newBuilder() {
+ return DEFAULT_INSTANCE.toBuilder();
+ }
+ public static Builder newBuilder(dev.cobalt.storage.StorageProto.LocalStorageEntry prototype) {
+ return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype);
+ }
+
+ /**
+ * <pre>
+ * A single local storage entry.
+ * </pre>
+ *
+ * Protobuf type {@code cobalt.storage.LocalStorageEntry}
+ */
+ public static final class Builder extends
+ com.google.protobuf.GeneratedMessageLite.Builder<
+ dev.cobalt.storage.StorageProto.LocalStorageEntry, Builder> implements
+ // @@protoc_insertion_point(builder_implements:cobalt.storage.LocalStorageEntry)
+ dev.cobalt.storage.StorageProto.LocalStorageEntryOrBuilder {
+ // Construct using dev.cobalt.storage.StorageProto.LocalStorageEntry.newBuilder()
+ private Builder() {
+ super(DEFAULT_INSTANCE);
+ }
+
+
+ /**
+ * <pre>
+ * The key for the local storage entry.
+ * </pre>
+ *
+ * <code>optional string key = 1;</code>
+ */
+ public java.lang.String getKey() {
+ return instance.getKey();
+ }
+ /**
+ * <pre>
+ * The key for the local storage entry.
+ * </pre>
+ *
+ * <code>optional string key = 1;</code>
+ */
+ public com.google.protobuf.ByteString
+ getKeyBytes() {
+ return instance.getKeyBytes();
+ }
+ /**
+ * <pre>
+ * The key for the local storage entry.
+ * </pre>
+ *
+ * <code>optional string key = 1;</code>
+ */
+ public Builder setKey(
+ java.lang.String value) {
+ copyOnWrite();
+ instance.setKey(value);
+ return this;
+ }
+ /**
+ * <pre>
+ * The key for the local storage entry.
+ * </pre>
+ *
+ * <code>optional string key = 1;</code>
+ */
+ public Builder clearKey() {
+ copyOnWrite();
+ instance.clearKey();
+ return this;
+ }
+ /**
+ * <pre>
+ * The key for the local storage entry.
+ * </pre>
+ *
+ * <code>optional string key = 1;</code>
+ */
+ public Builder setKeyBytes(
+ com.google.protobuf.ByteString value) {
+ copyOnWrite();
+ instance.setKeyBytes(value);
+ return this;
+ }
+
+ /**
+ * <pre>
+ * The value of the local storage entry.
+ * </pre>
+ *
+ * <code>optional string value = 2;</code>
+ */
+ public java.lang.String getValue() {
+ return instance.getValue();
+ }
+ /**
+ * <pre>
+ * The value of the local storage entry.
+ * </pre>
+ *
+ * <code>optional string value = 2;</code>
+ */
+ public com.google.protobuf.ByteString
+ getValueBytes() {
+ return instance.getValueBytes();
+ }
+ /**
+ * <pre>
+ * The value of the local storage entry.
+ * </pre>
+ *
+ * <code>optional string value = 2;</code>
+ */
+ public Builder setValue(
+ java.lang.String value) {
+ copyOnWrite();
+ instance.setValue(value);
+ return this;
+ }
+ /**
+ * <pre>
+ * The value of the local storage entry.
+ * </pre>
+ *
+ * <code>optional string value = 2;</code>
+ */
+ public Builder clearValue() {
+ copyOnWrite();
+ instance.clearValue();
+ return this;
+ }
+ /**
+ * <pre>
+ * The value of the local storage entry.
+ * </pre>
+ *
+ * <code>optional string value = 2;</code>
+ */
+ public Builder setValueBytes(
+ com.google.protobuf.ByteString value) {
+ copyOnWrite();
+ instance.setValueBytes(value);
+ return this;
+ }
+
+ // @@protoc_insertion_point(builder_scope:cobalt.storage.LocalStorageEntry)
+ }
+ protected final Object dynamicMethod(
+ com.google.protobuf.GeneratedMessageLite.MethodToInvoke method,
+ Object arg0, Object arg1) {
+ switch (method) {
+ case NEW_MUTABLE_INSTANCE: {
+ return new dev.cobalt.storage.StorageProto.LocalStorageEntry();
+ }
+ case IS_INITIALIZED: {
+ return DEFAULT_INSTANCE;
+ }
+ case MAKE_IMMUTABLE: {
+ return null;
+ }
+ case NEW_BUILDER: {
+ return new Builder();
+ }
+ case VISIT: {
+ Visitor visitor = (Visitor) arg0;
+ dev.cobalt.storage.StorageProto.LocalStorageEntry other = (dev.cobalt.storage.StorageProto.LocalStorageEntry) arg1;
+ key_ = visitor.visitString(!key_.isEmpty(), key_,
+ !other.key_.isEmpty(), other.key_);
+ value_ = visitor.visitString(!value_.isEmpty(), value_,
+ !other.value_.isEmpty(), other.value_);
+ if (visitor == com.google.protobuf.GeneratedMessageLite.MergeFromVisitor
+ .INSTANCE) {
+ }
+ return this;
+ }
+ case MERGE_FROM_STREAM: {
+ com.google.protobuf.CodedInputStream input =
+ (com.google.protobuf.CodedInputStream) arg0;
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry =
+ (com.google.protobuf.ExtensionRegistryLite) arg1;
+ try {
+ boolean done = false;
+ while (!done) {
+ int tag = input.readTag();
+ switch (tag) {
+ case 0:
+ done = true;
+ break;
+ default: {
+ if (!input.skipField(tag)) {
+ done = true;
+ }
+ break;
+ }
+ case 10: {
+ String s = input.readStringRequireUtf8();
+
+ key_ = s;
+ break;
+ }
+ case 18: {
+ String s = input.readStringRequireUtf8();
+
+ value_ = s;
+ break;
+ }
+ }
+ }
+ } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+ throw new RuntimeException(e.setUnfinishedMessage(this));
+ } catch (java.io.IOException e) {
+ throw new RuntimeException(
+ new com.google.protobuf.InvalidProtocolBufferException(
+ e.getMessage()).setUnfinishedMessage(this));
+ } finally {
+ }
+ }
+ case GET_DEFAULT_INSTANCE: {
+ return DEFAULT_INSTANCE;
+ }
+ case GET_PARSER: {
+ if (PARSER == null) { synchronized (dev.cobalt.storage.StorageProto.LocalStorageEntry.class) {
+ if (PARSER == null) {
+ PARSER = new DefaultInstanceBasedParser(DEFAULT_INSTANCE);
+ }
+ }
+ }
+ return PARSER;
+ }
+ }
+ throw new UnsupportedOperationException();
+ }
+
+
+ // @@protoc_insertion_point(class_scope:cobalt.storage.LocalStorageEntry)
+ private static final dev.cobalt.storage.StorageProto.LocalStorageEntry DEFAULT_INSTANCE;
+ static {
+ DEFAULT_INSTANCE = new LocalStorageEntry();
+ DEFAULT_INSTANCE.makeImmutable();
+ }
+
+ public static dev.cobalt.storage.StorageProto.LocalStorageEntry getDefaultInstance() {
+ return DEFAULT_INSTANCE;
+ }
+
+ private static volatile com.google.protobuf.Parser<LocalStorageEntry> PARSER;
+
+ public static com.google.protobuf.Parser<LocalStorageEntry> parser() {
+ return DEFAULT_INSTANCE.getParserForType();
+ }
+ }
+
+ public interface LocalStorageOrBuilder extends
+ // @@protoc_insertion_point(interface_extends:cobalt.storage.LocalStorage)
+ com.google.protobuf.MessageLiteOrBuilder {
+
+ /**
+ * <pre>
+ * A serialzied origin as defined in:
+ * https://html.spec.whatwg.org/multipage/origin.html#ascii-serialisation-of-an-origin.
+ * For example: "https://www.youtube.com"
+ * </pre>
+ *
+ * <code>optional string serialized_origin = 1;</code>
+ */
+ java.lang.String getSerializedOrigin();
+ /**
+ * <pre>
+ * A serialzied origin as defined in:
+ * https://html.spec.whatwg.org/multipage/origin.html#ascii-serialisation-of-an-origin.
+ * For example: "https://www.youtube.com"
+ * </pre>
+ *
+ * <code>optional string serialized_origin = 1;</code>
+ */
+ com.google.protobuf.ByteString
+ getSerializedOriginBytes();
+
+ /**
+ * <pre>
+ * The local storage entries for individual local storage.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+ */
+ java.util.List<dev.cobalt.storage.StorageProto.LocalStorageEntry>
+ getLocalStorageEntriesList();
+ /**
+ * <pre>
+ * The local storage entries for individual local storage.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+ */
+ dev.cobalt.storage.StorageProto.LocalStorageEntry getLocalStorageEntries(int index);
+ /**
+ * <pre>
+ * The local storage entries for individual local storage.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+ */
+ int getLocalStorageEntriesCount();
+ }
+ /**
+ * <pre>
+ * Multiple local storages identified by unique id.
+ * </pre>
+ *
+ * Protobuf type {@code cobalt.storage.LocalStorage}
+ */
+ public static final class LocalStorage extends
+ com.google.protobuf.GeneratedMessageLite<
+ LocalStorage, LocalStorage.Builder> implements
+ // @@protoc_insertion_point(message_implements:cobalt.storage.LocalStorage)
+ LocalStorageOrBuilder {
+ private LocalStorage() {
+ serializedOrigin_ = "";
+ localStorageEntries_ = emptyProtobufList();
+ }
+ private int bitField0_;
+ public static final int SERIALIZED_ORIGIN_FIELD_NUMBER = 1;
+ private java.lang.String serializedOrigin_;
+ /**
+ * <pre>
+ * A serialzied origin as defined in:
+ * https://html.spec.whatwg.org/multipage/origin.html#ascii-serialisation-of-an-origin.
+ * For example: "https://www.youtube.com"
+ * </pre>
+ *
+ * <code>optional string serialized_origin = 1;</code>
+ */
+ public java.lang.String getSerializedOrigin() {
+ return serializedOrigin_;
+ }
+ /**
+ * <pre>
+ * A serialzied origin as defined in:
+ * https://html.spec.whatwg.org/multipage/origin.html#ascii-serialisation-of-an-origin.
+ * For example: "https://www.youtube.com"
+ * </pre>
+ *
+ * <code>optional string serialized_origin = 1;</code>
+ */
+ public com.google.protobuf.ByteString
+ getSerializedOriginBytes() {
+ return com.google.protobuf.ByteString.copyFromUtf8(serializedOrigin_);
+ }
+ /**
+ * <pre>
+ * A serialzied origin as defined in:
+ * https://html.spec.whatwg.org/multipage/origin.html#ascii-serialisation-of-an-origin.
+ * For example: "https://www.youtube.com"
+ * </pre>
+ *
+ * <code>optional string serialized_origin = 1;</code>
+ */
+ private void setSerializedOrigin(
+ java.lang.String value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+
+ serializedOrigin_ = value;
+ }
+ /**
+ * <pre>
+ * A serialzied origin as defined in:
+ * https://html.spec.whatwg.org/multipage/origin.html#ascii-serialisation-of-an-origin.
+ * For example: "https://www.youtube.com"
+ * </pre>
+ *
+ * <code>optional string serialized_origin = 1;</code>
+ */
+ private void clearSerializedOrigin() {
+
+ serializedOrigin_ = getDefaultInstance().getSerializedOrigin();
+ }
+ /**
+ * <pre>
+ * A serialzied origin as defined in:
+ * https://html.spec.whatwg.org/multipage/origin.html#ascii-serialisation-of-an-origin.
+ * For example: "https://www.youtube.com"
+ * </pre>
+ *
+ * <code>optional string serialized_origin = 1;</code>
+ */
+ private void setSerializedOriginBytes(
+ com.google.protobuf.ByteString value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ checkByteStringIsUtf8(value);
+
+ serializedOrigin_ = value.toStringUtf8();
+ }
+
+ public static final int LOCAL_STORAGE_ENTRIES_FIELD_NUMBER = 2;
+ private com.google.protobuf.Internal.ProtobufList<dev.cobalt.storage.StorageProto.LocalStorageEntry> localStorageEntries_;
+ /**
+ * <pre>
+ * The local storage entries for individual local storage.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+ */
+ public java.util.List<dev.cobalt.storage.StorageProto.LocalStorageEntry> getLocalStorageEntriesList() {
+ return localStorageEntries_;
+ }
+ /**
+ * <pre>
+ * The local storage entries for individual local storage.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+ */
+ public java.util.List<? extends dev.cobalt.storage.StorageProto.LocalStorageEntryOrBuilder>
+ getLocalStorageEntriesOrBuilderList() {
+ return localStorageEntries_;
+ }
+ /**
+ * <pre>
+ * The local storage entries for individual local storage.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+ */
+ public int getLocalStorageEntriesCount() {
+ return localStorageEntries_.size();
+ }
+ /**
+ * <pre>
+ * The local storage entries for individual local storage.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+ */
+ public dev.cobalt.storage.StorageProto.LocalStorageEntry getLocalStorageEntries(int index) {
+ return localStorageEntries_.get(index);
+ }
+ /**
+ * <pre>
+ * The local storage entries for individual local storage.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+ */
+ public dev.cobalt.storage.StorageProto.LocalStorageEntryOrBuilder getLocalStorageEntriesOrBuilder(
+ int index) {
+ return localStorageEntries_.get(index);
+ }
+ private void ensureLocalStorageEntriesIsMutable() {
+ if (!localStorageEntries_.isModifiable()) {
+ localStorageEntries_ =
+ com.google.protobuf.GeneratedMessageLite.mutableCopy(localStorageEntries_);
+ }
+ }
+
+ /**
+ * <pre>
+ * The local storage entries for individual local storage.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+ */
+ private void setLocalStorageEntries(
+ int index, dev.cobalt.storage.StorageProto.LocalStorageEntry value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ ensureLocalStorageEntriesIsMutable();
+ localStorageEntries_.set(index, value);
+ }
+ /**
+ * <pre>
+ * The local storage entries for individual local storage.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+ */
+ private void setLocalStorageEntries(
+ int index, dev.cobalt.storage.StorageProto.LocalStorageEntry.Builder builderForValue) {
+ ensureLocalStorageEntriesIsMutable();
+ localStorageEntries_.set(index, builderForValue.build());
+ }
+ /**
+ * <pre>
+ * The local storage entries for individual local storage.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+ */
+ private void addLocalStorageEntries(dev.cobalt.storage.StorageProto.LocalStorageEntry value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ ensureLocalStorageEntriesIsMutable();
+ localStorageEntries_.add(value);
+ }
+ /**
+ * <pre>
+ * The local storage entries for individual local storage.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+ */
+ private void addLocalStorageEntries(
+ int index, dev.cobalt.storage.StorageProto.LocalStorageEntry value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ ensureLocalStorageEntriesIsMutable();
+ localStorageEntries_.add(index, value);
+ }
+ /**
+ * <pre>
+ * The local storage entries for individual local storage.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+ */
+ private void addLocalStorageEntries(
+ dev.cobalt.storage.StorageProto.LocalStorageEntry.Builder builderForValue) {
+ ensureLocalStorageEntriesIsMutable();
+ localStorageEntries_.add(builderForValue.build());
+ }
+ /**
+ * <pre>
+ * The local storage entries for individual local storage.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+ */
+ private void addLocalStorageEntries(
+ int index, dev.cobalt.storage.StorageProto.LocalStorageEntry.Builder builderForValue) {
+ ensureLocalStorageEntriesIsMutable();
+ localStorageEntries_.add(index, builderForValue.build());
+ }
+ /**
+ * <pre>
+ * The local storage entries for individual local storage.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+ */
+ private void addAllLocalStorageEntries(
+ java.lang.Iterable<? extends dev.cobalt.storage.StorageProto.LocalStorageEntry> values) {
+ ensureLocalStorageEntriesIsMutable();
+ com.google.protobuf.AbstractMessageLite.addAll(
+ values, localStorageEntries_);
+ }
+ /**
+ * <pre>
+ * The local storage entries for individual local storage.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+ */
+ private void clearLocalStorageEntries() {
+ localStorageEntries_ = emptyProtobufList();
+ }
+ /**
+ * <pre>
+ * The local storage entries for individual local storage.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+ */
+ private void removeLocalStorageEntries(int index) {
+ ensureLocalStorageEntriesIsMutable();
+ localStorageEntries_.remove(index);
+ }
+
+ public void writeTo(com.google.protobuf.CodedOutputStream output)
+ throws java.io.IOException {
+ if (!serializedOrigin_.isEmpty()) {
+ output.writeString(1, getSerializedOrigin());
+ }
+ for (int i = 0; i < localStorageEntries_.size(); i++) {
+ output.writeMessage(2, localStorageEntries_.get(i));
+ }
+ }
+
+ public int getSerializedSize() {
+ int size = memoizedSerializedSize;
+ if (size != -1) return size;
+
+ size = 0;
+ if (!serializedOrigin_.isEmpty()) {
+ size += com.google.protobuf.CodedOutputStream
+ .computeStringSize(1, getSerializedOrigin());
+ }
+ for (int i = 0; i < localStorageEntries_.size(); i++) {
+ size += com.google.protobuf.CodedOutputStream
+ .computeMessageSize(2, localStorageEntries_.get(i));
+ }
+ memoizedSerializedSize = size;
+ return size;
+ }
+
+ public static dev.cobalt.storage.StorageProto.LocalStorage parseFrom(
+ com.google.protobuf.ByteString data)
+ throws com.google.protobuf.InvalidProtocolBufferException {
+ return com.google.protobuf.GeneratedMessageLite.parseFrom(
+ DEFAULT_INSTANCE, data);
+ }
+ public static dev.cobalt.storage.StorageProto.LocalStorage parseFrom(
+ com.google.protobuf.ByteString data,
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws com.google.protobuf.InvalidProtocolBufferException {
+ return com.google.protobuf.GeneratedMessageLite.parseFrom(
+ DEFAULT_INSTANCE, data, extensionRegistry);
+ }
+ public static dev.cobalt.storage.StorageProto.LocalStorage parseFrom(byte[] data)
+ throws com.google.protobuf.InvalidProtocolBufferException {
+ return com.google.protobuf.GeneratedMessageLite.parseFrom(
+ DEFAULT_INSTANCE, data);
+ }
+ public static dev.cobalt.storage.StorageProto.LocalStorage parseFrom(
+ byte[] data,
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws com.google.protobuf.InvalidProtocolBufferException {
+ return com.google.protobuf.GeneratedMessageLite.parseFrom(
+ DEFAULT_INSTANCE, data, extensionRegistry);
+ }
+ public static dev.cobalt.storage.StorageProto.LocalStorage parseFrom(java.io.InputStream input)
+ throws java.io.IOException {
+ return com.google.protobuf.GeneratedMessageLite.parseFrom(
+ DEFAULT_INSTANCE, input);
+ }
+ public static dev.cobalt.storage.StorageProto.LocalStorage parseFrom(
+ java.io.InputStream input,
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws java.io.IOException {
+ return com.google.protobuf.GeneratedMessageLite.parseFrom(
+ DEFAULT_INSTANCE, input, extensionRegistry);
+ }
+ public static dev.cobalt.storage.StorageProto.LocalStorage parseDelimitedFrom(java.io.InputStream input)
+ throws java.io.IOException {
+ return parseDelimitedFrom(DEFAULT_INSTANCE, input);
+ }
+ public static dev.cobalt.storage.StorageProto.LocalStorage parseDelimitedFrom(
+ java.io.InputStream input,
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws java.io.IOException {
+ return parseDelimitedFrom(DEFAULT_INSTANCE, input, extensionRegistry);
+ }
+ public static dev.cobalt.storage.StorageProto.LocalStorage parseFrom(
+ com.google.protobuf.CodedInputStream input)
+ throws java.io.IOException {
+ return com.google.protobuf.GeneratedMessageLite.parseFrom(
+ DEFAULT_INSTANCE, input);
+ }
+ public static dev.cobalt.storage.StorageProto.LocalStorage parseFrom(
+ com.google.protobuf.CodedInputStream input,
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws java.io.IOException {
+ return com.google.protobuf.GeneratedMessageLite.parseFrom(
+ DEFAULT_INSTANCE, input, extensionRegistry);
+ }
+
+ public static Builder newBuilder() {
+ return DEFAULT_INSTANCE.toBuilder();
+ }
+ public static Builder newBuilder(dev.cobalt.storage.StorageProto.LocalStorage prototype) {
+ return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype);
+ }
+
+ /**
+ * <pre>
+ * Multiple local storages identified by unique id.
+ * </pre>
+ *
+ * Protobuf type {@code cobalt.storage.LocalStorage}
+ */
+ public static final class Builder extends
+ com.google.protobuf.GeneratedMessageLite.Builder<
+ dev.cobalt.storage.StorageProto.LocalStorage, Builder> implements
+ // @@protoc_insertion_point(builder_implements:cobalt.storage.LocalStorage)
+ dev.cobalt.storage.StorageProto.LocalStorageOrBuilder {
+ // Construct using dev.cobalt.storage.StorageProto.LocalStorage.newBuilder()
+ private Builder() {
+ super(DEFAULT_INSTANCE);
+ }
+
+
+ /**
+ * <pre>
+ * A serialzied origin as defined in:
+ * https://html.spec.whatwg.org/multipage/origin.html#ascii-serialisation-of-an-origin.
+ * For example: "https://www.youtube.com"
+ * </pre>
+ *
+ * <code>optional string serialized_origin = 1;</code>
+ */
+ public java.lang.String getSerializedOrigin() {
+ return instance.getSerializedOrigin();
+ }
+ /**
+ * <pre>
+ * A serialzied origin as defined in:
+ * https://html.spec.whatwg.org/multipage/origin.html#ascii-serialisation-of-an-origin.
+ * For example: "https://www.youtube.com"
+ * </pre>
+ *
+ * <code>optional string serialized_origin = 1;</code>
+ */
+ public com.google.protobuf.ByteString
+ getSerializedOriginBytes() {
+ return instance.getSerializedOriginBytes();
+ }
+ /**
+ * <pre>
+ * A serialzied origin as defined in:
+ * https://html.spec.whatwg.org/multipage/origin.html#ascii-serialisation-of-an-origin.
+ * For example: "https://www.youtube.com"
+ * </pre>
+ *
+ * <code>optional string serialized_origin = 1;</code>
+ */
+ public Builder setSerializedOrigin(
+ java.lang.String value) {
+ copyOnWrite();
+ instance.setSerializedOrigin(value);
+ return this;
+ }
+ /**
+ * <pre>
+ * A serialzied origin as defined in:
+ * https://html.spec.whatwg.org/multipage/origin.html#ascii-serialisation-of-an-origin.
+ * For example: "https://www.youtube.com"
+ * </pre>
+ *
+ * <code>optional string serialized_origin = 1;</code>
+ */
+ public Builder clearSerializedOrigin() {
+ copyOnWrite();
+ instance.clearSerializedOrigin();
+ return this;
+ }
+ /**
+ * <pre>
+ * A serialzied origin as defined in:
+ * https://html.spec.whatwg.org/multipage/origin.html#ascii-serialisation-of-an-origin.
+ * For example: "https://www.youtube.com"
+ * </pre>
+ *
+ * <code>optional string serialized_origin = 1;</code>
+ */
+ public Builder setSerializedOriginBytes(
+ com.google.protobuf.ByteString value) {
+ copyOnWrite();
+ instance.setSerializedOriginBytes(value);
+ return this;
+ }
+
+ /**
+ * <pre>
+ * The local storage entries for individual local storage.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+ */
+ public java.util.List<dev.cobalt.storage.StorageProto.LocalStorageEntry> getLocalStorageEntriesList() {
+ return java.util.Collections.unmodifiableList(
+ instance.getLocalStorageEntriesList());
+ }
+ /**
+ * <pre>
+ * The local storage entries for individual local storage.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+ */
+ public int getLocalStorageEntriesCount() {
+ return instance.getLocalStorageEntriesCount();
+ }/**
+ * <pre>
+ * The local storage entries for individual local storage.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+ */
+ public dev.cobalt.storage.StorageProto.LocalStorageEntry getLocalStorageEntries(int index) {
+ return instance.getLocalStorageEntries(index);
+ }
+ /**
+ * <pre>
+ * The local storage entries for individual local storage.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+ */
+ public Builder setLocalStorageEntries(
+ int index, dev.cobalt.storage.StorageProto.LocalStorageEntry value) {
+ copyOnWrite();
+ instance.setLocalStorageEntries(index, value);
+ return this;
+ }
+ /**
+ * <pre>
+ * The local storage entries for individual local storage.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+ */
+ public Builder setLocalStorageEntries(
+ int index, dev.cobalt.storage.StorageProto.LocalStorageEntry.Builder builderForValue) {
+ copyOnWrite();
+ instance.setLocalStorageEntries(index, builderForValue);
+ return this;
+ }
+ /**
+ * <pre>
+ * The local storage entries for individual local storage.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+ */
+ public Builder addLocalStorageEntries(dev.cobalt.storage.StorageProto.LocalStorageEntry value) {
+ copyOnWrite();
+ instance.addLocalStorageEntries(value);
+ return this;
+ }
+ /**
+ * <pre>
+ * The local storage entries for individual local storage.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+ */
+ public Builder addLocalStorageEntries(
+ int index, dev.cobalt.storage.StorageProto.LocalStorageEntry value) {
+ copyOnWrite();
+ instance.addLocalStorageEntries(index, value);
+ return this;
+ }
+ /**
+ * <pre>
+ * The local storage entries for individual local storage.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+ */
+ public Builder addLocalStorageEntries(
+ dev.cobalt.storage.StorageProto.LocalStorageEntry.Builder builderForValue) {
+ copyOnWrite();
+ instance.addLocalStorageEntries(builderForValue);
+ return this;
+ }
+ /**
+ * <pre>
+ * The local storage entries for individual local storage.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+ */
+ public Builder addLocalStorageEntries(
+ int index, dev.cobalt.storage.StorageProto.LocalStorageEntry.Builder builderForValue) {
+ copyOnWrite();
+ instance.addLocalStorageEntries(index, builderForValue);
+ return this;
+ }
+ /**
+ * <pre>
+ * The local storage entries for individual local storage.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+ */
+ public Builder addAllLocalStorageEntries(
+ java.lang.Iterable<? extends dev.cobalt.storage.StorageProto.LocalStorageEntry> values) {
+ copyOnWrite();
+ instance.addAllLocalStorageEntries(values);
+ return this;
+ }
+ /**
+ * <pre>
+ * The local storage entries for individual local storage.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+ */
+ public Builder clearLocalStorageEntries() {
+ copyOnWrite();
+ instance.clearLocalStorageEntries();
+ return this;
+ }
+ /**
+ * <pre>
+ * The local storage entries for individual local storage.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+ */
+ public Builder removeLocalStorageEntries(int index) {
+ copyOnWrite();
+ instance.removeLocalStorageEntries(index);
+ return this;
+ }
+
+ // @@protoc_insertion_point(builder_scope:cobalt.storage.LocalStorage)
+ }
+ protected final Object dynamicMethod(
+ com.google.protobuf.GeneratedMessageLite.MethodToInvoke method,
+ Object arg0, Object arg1) {
+ switch (method) {
+ case NEW_MUTABLE_INSTANCE: {
+ return new dev.cobalt.storage.StorageProto.LocalStorage();
+ }
+ case IS_INITIALIZED: {
+ return DEFAULT_INSTANCE;
+ }
+ case MAKE_IMMUTABLE: {
+ localStorageEntries_.makeImmutable();
+ return null;
+ }
+ case NEW_BUILDER: {
+ return new Builder();
+ }
+ case VISIT: {
+ Visitor visitor = (Visitor) arg0;
+ dev.cobalt.storage.StorageProto.LocalStorage other = (dev.cobalt.storage.StorageProto.LocalStorage) arg1;
+ serializedOrigin_ = visitor.visitString(!serializedOrigin_.isEmpty(), serializedOrigin_,
+ !other.serializedOrigin_.isEmpty(), other.serializedOrigin_);
+ localStorageEntries_= visitor.visitList(localStorageEntries_, other.localStorageEntries_);
+ if (visitor == com.google.protobuf.GeneratedMessageLite.MergeFromVisitor
+ .INSTANCE) {
+ bitField0_ |= other.bitField0_;
+ }
+ return this;
+ }
+ case MERGE_FROM_STREAM: {
+ com.google.protobuf.CodedInputStream input =
+ (com.google.protobuf.CodedInputStream) arg0;
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry =
+ (com.google.protobuf.ExtensionRegistryLite) arg1;
+ try {
+ boolean done = false;
+ while (!done) {
+ int tag = input.readTag();
+ switch (tag) {
+ case 0:
+ done = true;
+ break;
+ default: {
+ if (!input.skipField(tag)) {
+ done = true;
+ }
+ break;
+ }
+ case 10: {
+ String s = input.readStringRequireUtf8();
+
+ serializedOrigin_ = s;
+ break;
+ }
+ case 18: {
+ if (!localStorageEntries_.isModifiable()) {
+ localStorageEntries_ =
+ com.google.protobuf.GeneratedMessageLite.mutableCopy(localStorageEntries_);
+ }
+ localStorageEntries_.add(
+ input.readMessage(dev.cobalt.storage.StorageProto.LocalStorageEntry.parser(), extensionRegistry));
+ break;
+ }
+ }
+ }
+ } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+ throw new RuntimeException(e.setUnfinishedMessage(this));
+ } catch (java.io.IOException e) {
+ throw new RuntimeException(
+ new com.google.protobuf.InvalidProtocolBufferException(
+ e.getMessage()).setUnfinishedMessage(this));
+ } finally {
+ }
+ }
+ case GET_DEFAULT_INSTANCE: {
+ return DEFAULT_INSTANCE;
+ }
+ case GET_PARSER: {
+ if (PARSER == null) { synchronized (dev.cobalt.storage.StorageProto.LocalStorage.class) {
+ if (PARSER == null) {
+ PARSER = new DefaultInstanceBasedParser(DEFAULT_INSTANCE);
+ }
+ }
+ }
+ return PARSER;
+ }
+ }
+ throw new UnsupportedOperationException();
+ }
+
+
+ // @@protoc_insertion_point(class_scope:cobalt.storage.LocalStorage)
+ private static final dev.cobalt.storage.StorageProto.LocalStorage DEFAULT_INSTANCE;
+ static {
+ DEFAULT_INSTANCE = new LocalStorage();
+ DEFAULT_INSTANCE.makeImmutable();
+ }
+
+ public static dev.cobalt.storage.StorageProto.LocalStorage getDefaultInstance() {
+ return DEFAULT_INSTANCE;
+ }
+
+ private static volatile com.google.protobuf.Parser<LocalStorage> PARSER;
+
+ public static com.google.protobuf.Parser<LocalStorage> parser() {
+ return DEFAULT_INSTANCE.getParserForType();
+ }
+ }
+
+ public interface StorageOrBuilder extends
+ // @@protoc_insertion_point(interface_extends:cobalt.storage.Storage)
+ com.google.protobuf.MessageLiteOrBuilder {
+
+ /**
+ * <pre>
+ * All the cookies.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+ */
+ java.util.List<dev.cobalt.storage.StorageProto.Cookie>
+ getCookiesList();
+ /**
+ * <pre>
+ * All the cookies.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+ */
+ dev.cobalt.storage.StorageProto.Cookie getCookies(int index);
+ /**
+ * <pre>
+ * All the cookies.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+ */
+ int getCookiesCount();
+
+ /**
+ * <pre>
+ * All local storages.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+ */
+ java.util.List<dev.cobalt.storage.StorageProto.LocalStorage>
+ getLocalStoragesList();
+ /**
+ * <pre>
+ * All local storages.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+ */
+ dev.cobalt.storage.StorageProto.LocalStorage getLocalStorages(int index);
+ /**
+ * <pre>
+ * All local storages.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+ */
+ int getLocalStoragesCount();
+ }
+ /**
+ * <pre>
+ * The full storage.
+ * </pre>
+ *
+ * Protobuf type {@code cobalt.storage.Storage}
+ */
+ public static final class Storage extends
+ com.google.protobuf.GeneratedMessageLite<
+ Storage, Storage.Builder> implements
+ // @@protoc_insertion_point(message_implements:cobalt.storage.Storage)
+ StorageOrBuilder {
+ private Storage() {
+ cookies_ = emptyProtobufList();
+ localStorages_ = emptyProtobufList();
+ }
+ public static final int COOKIES_FIELD_NUMBER = 1;
+ private com.google.protobuf.Internal.ProtobufList<dev.cobalt.storage.StorageProto.Cookie> cookies_;
+ /**
+ * <pre>
+ * All the cookies.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+ */
+ public java.util.List<dev.cobalt.storage.StorageProto.Cookie> getCookiesList() {
+ return cookies_;
+ }
+ /**
+ * <pre>
+ * All the cookies.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+ */
+ public java.util.List<? extends dev.cobalt.storage.StorageProto.CookieOrBuilder>
+ getCookiesOrBuilderList() {
+ return cookies_;
+ }
+ /**
+ * <pre>
+ * All the cookies.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+ */
+ public int getCookiesCount() {
+ return cookies_.size();
+ }
+ /**
+ * <pre>
+ * All the cookies.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+ */
+ public dev.cobalt.storage.StorageProto.Cookie getCookies(int index) {
+ return cookies_.get(index);
+ }
+ /**
+ * <pre>
+ * All the cookies.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+ */
+ public dev.cobalt.storage.StorageProto.CookieOrBuilder getCookiesOrBuilder(
+ int index) {
+ return cookies_.get(index);
+ }
+ private void ensureCookiesIsMutable() {
+ if (!cookies_.isModifiable()) {
+ cookies_ =
+ com.google.protobuf.GeneratedMessageLite.mutableCopy(cookies_);
+ }
+ }
+
+ /**
+ * <pre>
+ * All the cookies.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+ */
+ private void setCookies(
+ int index, dev.cobalt.storage.StorageProto.Cookie value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ ensureCookiesIsMutable();
+ cookies_.set(index, value);
+ }
+ /**
+ * <pre>
+ * All the cookies.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+ */
+ private void setCookies(
+ int index, dev.cobalt.storage.StorageProto.Cookie.Builder builderForValue) {
+ ensureCookiesIsMutable();
+ cookies_.set(index, builderForValue.build());
+ }
+ /**
+ * <pre>
+ * All the cookies.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+ */
+ private void addCookies(dev.cobalt.storage.StorageProto.Cookie value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ ensureCookiesIsMutable();
+ cookies_.add(value);
+ }
+ /**
+ * <pre>
+ * All the cookies.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+ */
+ private void addCookies(
+ int index, dev.cobalt.storage.StorageProto.Cookie value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ ensureCookiesIsMutable();
+ cookies_.add(index, value);
+ }
+ /**
+ * <pre>
+ * All the cookies.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+ */
+ private void addCookies(
+ dev.cobalt.storage.StorageProto.Cookie.Builder builderForValue) {
+ ensureCookiesIsMutable();
+ cookies_.add(builderForValue.build());
+ }
+ /**
+ * <pre>
+ * All the cookies.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+ */
+ private void addCookies(
+ int index, dev.cobalt.storage.StorageProto.Cookie.Builder builderForValue) {
+ ensureCookiesIsMutable();
+ cookies_.add(index, builderForValue.build());
+ }
+ /**
+ * <pre>
+ * All the cookies.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+ */
+ private void addAllCookies(
+ java.lang.Iterable<? extends dev.cobalt.storage.StorageProto.Cookie> values) {
+ ensureCookiesIsMutable();
+ com.google.protobuf.AbstractMessageLite.addAll(
+ values, cookies_);
+ }
+ /**
+ * <pre>
+ * All the cookies.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+ */
+ private void clearCookies() {
+ cookies_ = emptyProtobufList();
+ }
+ /**
+ * <pre>
+ * All the cookies.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+ */
+ private void removeCookies(int index) {
+ ensureCookiesIsMutable();
+ cookies_.remove(index);
+ }
+
+ public static final int LOCAL_STORAGES_FIELD_NUMBER = 2;
+ private com.google.protobuf.Internal.ProtobufList<dev.cobalt.storage.StorageProto.LocalStorage> localStorages_;
+ /**
+ * <pre>
+ * All local storages.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+ */
+ public java.util.List<dev.cobalt.storage.StorageProto.LocalStorage> getLocalStoragesList() {
+ return localStorages_;
+ }
+ /**
+ * <pre>
+ * All local storages.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+ */
+ public java.util.List<? extends dev.cobalt.storage.StorageProto.LocalStorageOrBuilder>
+ getLocalStoragesOrBuilderList() {
+ return localStorages_;
+ }
+ /**
+ * <pre>
+ * All local storages.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+ */
+ public int getLocalStoragesCount() {
+ return localStorages_.size();
+ }
+ /**
+ * <pre>
+ * All local storages.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+ */
+ public dev.cobalt.storage.StorageProto.LocalStorage getLocalStorages(int index) {
+ return localStorages_.get(index);
+ }
+ /**
+ * <pre>
+ * All local storages.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+ */
+ public dev.cobalt.storage.StorageProto.LocalStorageOrBuilder getLocalStoragesOrBuilder(
+ int index) {
+ return localStorages_.get(index);
+ }
+ private void ensureLocalStoragesIsMutable() {
+ if (!localStorages_.isModifiable()) {
+ localStorages_ =
+ com.google.protobuf.GeneratedMessageLite.mutableCopy(localStorages_);
+ }
+ }
+
+ /**
+ * <pre>
+ * All local storages.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+ */
+ private void setLocalStorages(
+ int index, dev.cobalt.storage.StorageProto.LocalStorage value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ ensureLocalStoragesIsMutable();
+ localStorages_.set(index, value);
+ }
+ /**
+ * <pre>
+ * All local storages.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+ */
+ private void setLocalStorages(
+ int index, dev.cobalt.storage.StorageProto.LocalStorage.Builder builderForValue) {
+ ensureLocalStoragesIsMutable();
+ localStorages_.set(index, builderForValue.build());
+ }
+ /**
+ * <pre>
+ * All local storages.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+ */
+ private void addLocalStorages(dev.cobalt.storage.StorageProto.LocalStorage value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ ensureLocalStoragesIsMutable();
+ localStorages_.add(value);
+ }
+ /**
+ * <pre>
+ * All local storages.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+ */
+ private void addLocalStorages(
+ int index, dev.cobalt.storage.StorageProto.LocalStorage value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ ensureLocalStoragesIsMutable();
+ localStorages_.add(index, value);
+ }
+ /**
+ * <pre>
+ * All local storages.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+ */
+ private void addLocalStorages(
+ dev.cobalt.storage.StorageProto.LocalStorage.Builder builderForValue) {
+ ensureLocalStoragesIsMutable();
+ localStorages_.add(builderForValue.build());
+ }
+ /**
+ * <pre>
+ * All local storages.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+ */
+ private void addLocalStorages(
+ int index, dev.cobalt.storage.StorageProto.LocalStorage.Builder builderForValue) {
+ ensureLocalStoragesIsMutable();
+ localStorages_.add(index, builderForValue.build());
+ }
+ /**
+ * <pre>
+ * All local storages.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+ */
+ private void addAllLocalStorages(
+ java.lang.Iterable<? extends dev.cobalt.storage.StorageProto.LocalStorage> values) {
+ ensureLocalStoragesIsMutable();
+ com.google.protobuf.AbstractMessageLite.addAll(
+ values, localStorages_);
+ }
+ /**
+ * <pre>
+ * All local storages.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+ */
+ private void clearLocalStorages() {
+ localStorages_ = emptyProtobufList();
+ }
+ /**
+ * <pre>
+ * All local storages.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+ */
+ private void removeLocalStorages(int index) {
+ ensureLocalStoragesIsMutable();
+ localStorages_.remove(index);
+ }
+
+ public void writeTo(com.google.protobuf.CodedOutputStream output)
+ throws java.io.IOException {
+ for (int i = 0; i < cookies_.size(); i++) {
+ output.writeMessage(1, cookies_.get(i));
+ }
+ for (int i = 0; i < localStorages_.size(); i++) {
+ output.writeMessage(2, localStorages_.get(i));
+ }
+ }
+
+ public int getSerializedSize() {
+ int size = memoizedSerializedSize;
+ if (size != -1) return size;
+
+ size = 0;
+ for (int i = 0; i < cookies_.size(); i++) {
+ size += com.google.protobuf.CodedOutputStream
+ .computeMessageSize(1, cookies_.get(i));
+ }
+ for (int i = 0; i < localStorages_.size(); i++) {
+ size += com.google.protobuf.CodedOutputStream
+ .computeMessageSize(2, localStorages_.get(i));
+ }
+ memoizedSerializedSize = size;
+ return size;
+ }
+
+ public static dev.cobalt.storage.StorageProto.Storage parseFrom(
+ com.google.protobuf.ByteString data)
+ throws com.google.protobuf.InvalidProtocolBufferException {
+ return com.google.protobuf.GeneratedMessageLite.parseFrom(
+ DEFAULT_INSTANCE, data);
+ }
+ public static dev.cobalt.storage.StorageProto.Storage parseFrom(
+ com.google.protobuf.ByteString data,
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws com.google.protobuf.InvalidProtocolBufferException {
+ return com.google.protobuf.GeneratedMessageLite.parseFrom(
+ DEFAULT_INSTANCE, data, extensionRegistry);
+ }
+ public static dev.cobalt.storage.StorageProto.Storage parseFrom(byte[] data)
+ throws com.google.protobuf.InvalidProtocolBufferException {
+ return com.google.protobuf.GeneratedMessageLite.parseFrom(
+ DEFAULT_INSTANCE, data);
+ }
+ public static dev.cobalt.storage.StorageProto.Storage parseFrom(
+ byte[] data,
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws com.google.protobuf.InvalidProtocolBufferException {
+ return com.google.protobuf.GeneratedMessageLite.parseFrom(
+ DEFAULT_INSTANCE, data, extensionRegistry);
+ }
+ public static dev.cobalt.storage.StorageProto.Storage parseFrom(java.io.InputStream input)
+ throws java.io.IOException {
+ return com.google.protobuf.GeneratedMessageLite.parseFrom(
+ DEFAULT_INSTANCE, input);
+ }
+ public static dev.cobalt.storage.StorageProto.Storage parseFrom(
+ java.io.InputStream input,
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws java.io.IOException {
+ return com.google.protobuf.GeneratedMessageLite.parseFrom(
+ DEFAULT_INSTANCE, input, extensionRegistry);
+ }
+ public static dev.cobalt.storage.StorageProto.Storage parseDelimitedFrom(java.io.InputStream input)
+ throws java.io.IOException {
+ return parseDelimitedFrom(DEFAULT_INSTANCE, input);
+ }
+ public static dev.cobalt.storage.StorageProto.Storage parseDelimitedFrom(
+ java.io.InputStream input,
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws java.io.IOException {
+ return parseDelimitedFrom(DEFAULT_INSTANCE, input, extensionRegistry);
+ }
+ public static dev.cobalt.storage.StorageProto.Storage parseFrom(
+ com.google.protobuf.CodedInputStream input)
+ throws java.io.IOException {
+ return com.google.protobuf.GeneratedMessageLite.parseFrom(
+ DEFAULT_INSTANCE, input);
+ }
+ public static dev.cobalt.storage.StorageProto.Storage parseFrom(
+ com.google.protobuf.CodedInputStream input,
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws java.io.IOException {
+ return com.google.protobuf.GeneratedMessageLite.parseFrom(
+ DEFAULT_INSTANCE, input, extensionRegistry);
+ }
+
+ public static Builder newBuilder() {
+ return DEFAULT_INSTANCE.toBuilder();
+ }
+ public static Builder newBuilder(dev.cobalt.storage.StorageProto.Storage prototype) {
+ return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype);
+ }
+
+ /**
+ * <pre>
+ * The full storage.
+ * </pre>
+ *
+ * Protobuf type {@code cobalt.storage.Storage}
+ */
+ public static final class Builder extends
+ com.google.protobuf.GeneratedMessageLite.Builder<
+ dev.cobalt.storage.StorageProto.Storage, Builder> implements
+ // @@protoc_insertion_point(builder_implements:cobalt.storage.Storage)
+ dev.cobalt.storage.StorageProto.StorageOrBuilder {
+ // Construct using dev.cobalt.storage.StorageProto.Storage.newBuilder()
+ private Builder() {
+ super(DEFAULT_INSTANCE);
+ }
+
+
+ /**
+ * <pre>
+ * All the cookies.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+ */
+ public java.util.List<dev.cobalt.storage.StorageProto.Cookie> getCookiesList() {
+ return java.util.Collections.unmodifiableList(
+ instance.getCookiesList());
+ }
+ /**
+ * <pre>
+ * All the cookies.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+ */
+ public int getCookiesCount() {
+ return instance.getCookiesCount();
+ }/**
+ * <pre>
+ * All the cookies.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+ */
+ public dev.cobalt.storage.StorageProto.Cookie getCookies(int index) {
+ return instance.getCookies(index);
+ }
+ /**
+ * <pre>
+ * All the cookies.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+ */
+ public Builder setCookies(
+ int index, dev.cobalt.storage.StorageProto.Cookie value) {
+ copyOnWrite();
+ instance.setCookies(index, value);
+ return this;
+ }
+ /**
+ * <pre>
+ * All the cookies.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+ */
+ public Builder setCookies(
+ int index, dev.cobalt.storage.StorageProto.Cookie.Builder builderForValue) {
+ copyOnWrite();
+ instance.setCookies(index, builderForValue);
+ return this;
+ }
+ /**
+ * <pre>
+ * All the cookies.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+ */
+ public Builder addCookies(dev.cobalt.storage.StorageProto.Cookie value) {
+ copyOnWrite();
+ instance.addCookies(value);
+ return this;
+ }
+ /**
+ * <pre>
+ * All the cookies.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+ */
+ public Builder addCookies(
+ int index, dev.cobalt.storage.StorageProto.Cookie value) {
+ copyOnWrite();
+ instance.addCookies(index, value);
+ return this;
+ }
+ /**
+ * <pre>
+ * All the cookies.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+ */
+ public Builder addCookies(
+ dev.cobalt.storage.StorageProto.Cookie.Builder builderForValue) {
+ copyOnWrite();
+ instance.addCookies(builderForValue);
+ return this;
+ }
+ /**
+ * <pre>
+ * All the cookies.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+ */
+ public Builder addCookies(
+ int index, dev.cobalt.storage.StorageProto.Cookie.Builder builderForValue) {
+ copyOnWrite();
+ instance.addCookies(index, builderForValue);
+ return this;
+ }
+ /**
+ * <pre>
+ * All the cookies.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+ */
+ public Builder addAllCookies(
+ java.lang.Iterable<? extends dev.cobalt.storage.StorageProto.Cookie> values) {
+ copyOnWrite();
+ instance.addAllCookies(values);
+ return this;
+ }
+ /**
+ * <pre>
+ * All the cookies.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+ */
+ public Builder clearCookies() {
+ copyOnWrite();
+ instance.clearCookies();
+ return this;
+ }
+ /**
+ * <pre>
+ * All the cookies.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+ */
+ public Builder removeCookies(int index) {
+ copyOnWrite();
+ instance.removeCookies(index);
+ return this;
+ }
+
+ /**
+ * <pre>
+ * All local storages.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+ */
+ public java.util.List<dev.cobalt.storage.StorageProto.LocalStorage> getLocalStoragesList() {
+ return java.util.Collections.unmodifiableList(
+ instance.getLocalStoragesList());
+ }
+ /**
+ * <pre>
+ * All local storages.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+ */
+ public int getLocalStoragesCount() {
+ return instance.getLocalStoragesCount();
+ }/**
+ * <pre>
+ * All local storages.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+ */
+ public dev.cobalt.storage.StorageProto.LocalStorage getLocalStorages(int index) {
+ return instance.getLocalStorages(index);
+ }
+ /**
+ * <pre>
+ * All local storages.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+ */
+ public Builder setLocalStorages(
+ int index, dev.cobalt.storage.StorageProto.LocalStorage value) {
+ copyOnWrite();
+ instance.setLocalStorages(index, value);
+ return this;
+ }
+ /**
+ * <pre>
+ * All local storages.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+ */
+ public Builder setLocalStorages(
+ int index, dev.cobalt.storage.StorageProto.LocalStorage.Builder builderForValue) {
+ copyOnWrite();
+ instance.setLocalStorages(index, builderForValue);
+ return this;
+ }
+ /**
+ * <pre>
+ * All local storages.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+ */
+ public Builder addLocalStorages(dev.cobalt.storage.StorageProto.LocalStorage value) {
+ copyOnWrite();
+ instance.addLocalStorages(value);
+ return this;
+ }
+ /**
+ * <pre>
+ * All local storages.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+ */
+ public Builder addLocalStorages(
+ int index, dev.cobalt.storage.StorageProto.LocalStorage value) {
+ copyOnWrite();
+ instance.addLocalStorages(index, value);
+ return this;
+ }
+ /**
+ * <pre>
+ * All local storages.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+ */
+ public Builder addLocalStorages(
+ dev.cobalt.storage.StorageProto.LocalStorage.Builder builderForValue) {
+ copyOnWrite();
+ instance.addLocalStorages(builderForValue);
+ return this;
+ }
+ /**
+ * <pre>
+ * All local storages.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+ */
+ public Builder addLocalStorages(
+ int index, dev.cobalt.storage.StorageProto.LocalStorage.Builder builderForValue) {
+ copyOnWrite();
+ instance.addLocalStorages(index, builderForValue);
+ return this;
+ }
+ /**
+ * <pre>
+ * All local storages.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+ */
+ public Builder addAllLocalStorages(
+ java.lang.Iterable<? extends dev.cobalt.storage.StorageProto.LocalStorage> values) {
+ copyOnWrite();
+ instance.addAllLocalStorages(values);
+ return this;
+ }
+ /**
+ * <pre>
+ * All local storages.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+ */
+ public Builder clearLocalStorages() {
+ copyOnWrite();
+ instance.clearLocalStorages();
+ return this;
+ }
+ /**
+ * <pre>
+ * All local storages.
+ * </pre>
+ *
+ * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+ */
+ public Builder removeLocalStorages(int index) {
+ copyOnWrite();
+ instance.removeLocalStorages(index);
+ return this;
+ }
+
+ // @@protoc_insertion_point(builder_scope:cobalt.storage.Storage)
+ }
+ protected final Object dynamicMethod(
+ com.google.protobuf.GeneratedMessageLite.MethodToInvoke method,
+ Object arg0, Object arg1) {
+ switch (method) {
+ case NEW_MUTABLE_INSTANCE: {
+ return new dev.cobalt.storage.StorageProto.Storage();
+ }
+ case IS_INITIALIZED: {
+ return DEFAULT_INSTANCE;
+ }
+ case MAKE_IMMUTABLE: {
+ cookies_.makeImmutable();
+ localStorages_.makeImmutable();
+ return null;
+ }
+ case NEW_BUILDER: {
+ return new Builder();
+ }
+ case VISIT: {
+ Visitor visitor = (Visitor) arg0;
+ dev.cobalt.storage.StorageProto.Storage other = (dev.cobalt.storage.StorageProto.Storage) arg1;
+ cookies_= visitor.visitList(cookies_, other.cookies_);
+ localStorages_= visitor.visitList(localStorages_, other.localStorages_);
+ if (visitor == com.google.protobuf.GeneratedMessageLite.MergeFromVisitor
+ .INSTANCE) {
+ }
+ return this;
+ }
+ case MERGE_FROM_STREAM: {
+ com.google.protobuf.CodedInputStream input =
+ (com.google.protobuf.CodedInputStream) arg0;
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry =
+ (com.google.protobuf.ExtensionRegistryLite) arg1;
+ try {
+ boolean done = false;
+ while (!done) {
+ int tag = input.readTag();
+ switch (tag) {
+ case 0:
+ done = true;
+ break;
+ default: {
+ if (!input.skipField(tag)) {
+ done = true;
+ }
+ break;
+ }
+ case 10: {
+ if (!cookies_.isModifiable()) {
+ cookies_ =
+ com.google.protobuf.GeneratedMessageLite.mutableCopy(cookies_);
+ }
+ cookies_.add(
+ input.readMessage(dev.cobalt.storage.StorageProto.Cookie.parser(), extensionRegistry));
+ break;
+ }
+ case 18: {
+ if (!localStorages_.isModifiable()) {
+ localStorages_ =
+ com.google.protobuf.GeneratedMessageLite.mutableCopy(localStorages_);
+ }
+ localStorages_.add(
+ input.readMessage(dev.cobalt.storage.StorageProto.LocalStorage.parser(), extensionRegistry));
+ break;
+ }
+ }
+ }
+ } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+ throw new RuntimeException(e.setUnfinishedMessage(this));
+ } catch (java.io.IOException e) {
+ throw new RuntimeException(
+ new com.google.protobuf.InvalidProtocolBufferException(
+ e.getMessage()).setUnfinishedMessage(this));
+ } finally {
+ }
+ }
+ case GET_DEFAULT_INSTANCE: {
+ return DEFAULT_INSTANCE;
+ }
+ case GET_PARSER: {
+ if (PARSER == null) { synchronized (dev.cobalt.storage.StorageProto.Storage.class) {
+ if (PARSER == null) {
+ PARSER = new DefaultInstanceBasedParser(DEFAULT_INSTANCE);
+ }
+ }
+ }
+ return PARSER;
+ }
+ }
+ throw new UnsupportedOperationException();
+ }
+
+
+ // @@protoc_insertion_point(class_scope:cobalt.storage.Storage)
+ private static final dev.cobalt.storage.StorageProto.Storage DEFAULT_INSTANCE;
+ static {
+ DEFAULT_INSTANCE = new Storage();
+ DEFAULT_INSTANCE.makeImmutable();
+ }
+
+ public static dev.cobalt.storage.StorageProto.Storage getDefaultInstance() {
+ return DEFAULT_INSTANCE;
+ }
+
+ private static volatile com.google.protobuf.Parser<Storage> PARSER;
+
+ public static com.google.protobuf.Parser<Storage> parser() {
+ return DEFAULT_INSTANCE.getParserForType();
+ }
+ }
+
+
+ static {
+ }
+
+ // @@protoc_insertion_point(outer_class_scope)
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/DisplayUtil.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/DisplayUtil.java
new file mode 100644
index 0000000..16f2b46
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/DisplayUtil.java
@@ -0,0 +1,82 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.util;
+
+import android.content.Context;
+import android.util.DisplayMetrics;
+import android.util.Size;
+import android.view.WindowManager;
+
+/** Utility functions for querying display attributes. */
+public class DisplayUtil {
+
+ private DisplayUtil() {}
+
+ /**
+ * Returns the size of the physical display size in pixels.
+ *
+ * <p>This differs from {@link #getSystemDisplaySize(Context)} because it only uses
+ * {@link DisplayMetrics}.
+ */
+ public static Size getDisplaySize(Context context) {
+ DisplayMetrics metrics = getDisplayMetrics(context);
+ return new Size(metrics.widthPixels, metrics.heightPixels);
+ }
+
+ /**
+ * Returns the size of the current physical display size in pixels.
+ *
+ * <p>This differs from {@link #getDisplaySize(Context)} because it allows the
+ * system property "sys.display-size" to override {@link DisplayMetrics}.
+ */
+ public static Size getSystemDisplaySize(Context context) {
+ Size widthAndHeightPx = getSystemDisplayWidthAndHeightPxInternal();
+ if (widthAndHeightPx == null) {
+ widthAndHeightPx = getDisplaySize(context);
+ }
+ return widthAndHeightPx;
+ }
+
+ /**
+ * Returns the size of the current physical display size in pixels.
+ * or {@code null} if unavailable.
+ */
+ private static Size getSystemDisplayWidthAndHeightPxInternal() {
+ final String displaySize = SystemPropertiesHelper.getString("sys.display-size");
+ if (displaySize != null) {
+ final String[] widthAndHeightPx = displaySize.split("x");
+ if (widthAndHeightPx.length == 2) {
+ try {
+ return new Size(
+ Integer.parseInt(widthAndHeightPx[0]), Integer.parseInt(widthAndHeightPx[1]));
+ } catch (NumberFormatException exception) {
+ // pass
+ }
+ }
+ }
+ return null;
+ }
+
+ private static DisplayMetrics cachedDisplayMetrics = null;
+
+ private static DisplayMetrics getDisplayMetrics(Context context) {
+ if (cachedDisplayMetrics == null) {
+ cachedDisplayMetrics = new DisplayMetrics();
+ ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE))
+ .getDefaultDisplay().getRealMetrics(cachedDisplayMetrics);
+ }
+ return cachedDisplayMetrics;
+ }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/Holder.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/Holder.java
new file mode 100644
index 0000000..7fa3407
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/Holder.java
@@ -0,0 +1,35 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.util;
+
+import android.support.annotation.Nullable;
+
+/** Holds a mutable reference to an object, or null. */
+public class Holder<T> {
+ private T instance;
+
+ public Holder() {
+ this.instance = null;
+ }
+
+ public void set(@Nullable T instance) {
+ this.instance = instance;
+ }
+
+ @Nullable
+ public T get() {
+ return instance;
+ }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/IsEmulator.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/IsEmulator.java
new file mode 100644
index 0000000..0ade400
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/IsEmulator.java
@@ -0,0 +1,29 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.util;
+
+import android.os.Build;
+
+/** A simple utility class to detect whether we're running in an emulator or not. */
+public class IsEmulator {
+ private IsEmulator() {}
+
+ public static boolean isEmulator() {
+ String qemu = System.getProperty("ro.kernel.qemu", "?");
+ return qemu.equals("1")
+ || Build.HARDWARE.contains("goldfish")
+ || Build.HARDWARE.contains("ranchu");
+ }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/Log.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/Log.java
new file mode 100644
index 0000000..59af6ac
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/Log.java
@@ -0,0 +1,71 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.util;
+
+/**
+ * API for sending Starboard log output. This uses a JNI helper rather than directly calling Android
+ * logging so that it remains in the app even when Android logging is stripped by Proguard.
+ */
+public final class Log {
+ public static final String TAG = "starboard";
+
+ private Log() {}
+
+ private static native int nativeWrite(char priority, String tag, String msg, Throwable tr);
+
+ public static int v(String tag, String msg) {
+ return nativeWrite('v', tag, msg, null);
+ }
+
+ public static int v(String tag, String msg, Throwable tr) {
+ return nativeWrite('v', tag, msg, tr);
+ }
+
+ public static int d(String tag, String msg) {
+ return nativeWrite('d', tag, msg, null);
+ }
+
+ public static int d(String tag, String msg, Throwable tr) {
+ return nativeWrite('d', tag, msg, tr);
+ }
+
+ public static int i(String tag, String msg) {
+ return nativeWrite('i', tag, msg, null);
+ }
+
+ public static int i(String tag, String msg, Throwable tr) {
+ return nativeWrite('i', tag, msg, tr);
+ }
+
+ public static int w(String tag, String msg) {
+ return nativeWrite('w', tag, msg, null);
+ }
+
+ public static int w(String tag, String msg, Throwable tr) {
+ return nativeWrite('w', tag, msg, tr);
+ }
+
+ public static int w(String tag, Throwable tr) {
+ return nativeWrite('w', tag, "", tr);
+ }
+
+ public static int e(String tag, String msg) {
+ return nativeWrite('e', tag, msg, null);
+ }
+
+ public static int e(String tag, String msg, Throwable tr) {
+ return nativeWrite('e', tag, msg, tr);
+ }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/SystemPropertiesHelper.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/SystemPropertiesHelper.java
new file mode 100644
index 0000000..90448c8
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/SystemPropertiesHelper.java
@@ -0,0 +1,49 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.util;
+
+import static dev.cobalt.util.Log.TAG;
+
+import java.lang.reflect.Method;
+
+/** Utility class for accessing system properties via reflection. */
+public class SystemPropertiesHelper {
+ private static Method getStringMethod;
+ static {
+ try {
+ getStringMethod = ClassLoader.getSystemClassLoader()
+ .loadClass("android.os.SystemProperties")
+ .getMethod("get", String.class);
+ if (getStringMethod == null) {
+ Log.e(TAG, "Couldn't load system properties getString");
+ }
+ } catch (Exception exception) {
+ Log.e(TAG, "Exception looking up system properties methods: ", exception);
+ }
+ }
+
+ private SystemPropertiesHelper() {}
+
+ public static String getString(String property) {
+ if (getStringMethod != null) {
+ try {
+ return (String) getStringMethod.invoke(null, new Object[] { property });
+ } catch (Exception exception) {
+ Log.e(TAG, "Exception getting system property: ", exception);
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/UsedByNative.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/UsedByNative.java
new file mode 100644
index 0000000..d78647d
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/UsedByNative.java
@@ -0,0 +1,25 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.util;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation used for marking methods and fields that are called from native code. Useful for
+ * keeping components that would otherwise be removed by Proguard.
+ */
+@Target({ElementType.METHOD, ElementType.FIELD, ElementType.TYPE, ElementType.CONSTRUCTOR})
+public @interface UsedByNative {}
diff --git a/src/starboard/android/apk/app/src/main/res/layout/coat_error_dialog.xml b/src/starboard/android/apk/app/src/main/res/layout/coat_error_dialog.xml
new file mode 100644
index 0000000..c3f6390
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/res/layout/coat_error_dialog.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2017 The Cobalt Authors. All Rights Reserved.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/error_frame"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:focusable="true"
+ android:focusableInTouchMode="true"
+ android:descendantFocusability="afterDescendants"
+ android:nextFocusLeft="@id/error_frame"
+ android:nextFocusRight="@id/error_frame"
+ android:nextFocusUp="@id/error_frame"
+ android:nextFocusDown="@id/error_frame">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:orientation="vertical">
+
+ <ImageView
+ android:id="@+id/image"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/lb_error_image_max_height"
+ android:layout_gravity="center" />
+ <TextView
+ android:id="@+id/message"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:maxWidth="@dimen/lb_error_message_max_width"
+ style="?attr/errorMessageStyle"/>
+ <Button
+ android:id="@+id/button_1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:visibility="gone"
+ style="?android:attr/buttonStyle"/>
+ <Button
+ android:id="@+id/button_2"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:visibility="gone"
+ style="?android:attr/buttonStyle"/>
+ <Button
+ android:id="@+id/button_3"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:visibility="gone"
+ style="?android:attr/buttonStyle"/>
+ </LinearLayout>
+
+</FrameLayout>
diff --git a/src/starboard/android/apk/app/src/main/res/values/colors.xml b/src/starboard/android/apk/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..30fde0c
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/res/values/colors.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2017 The Cobalt Authors. All Rights Reserved.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <color name="cobalt_blue">#ff16396b</color>
+ <color name="primary">@color/cobalt_blue</color>
+</resources>
diff --git a/src/starboard/android/apk/app/src/main/res/values/ids.xml b/src/starboard/android/apk/app/src/main/res/values/ids.xml
new file mode 100644
index 0000000..8097776
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/res/values/ids.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2017 The Cobalt Authors. All Rights Reserved.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <item name="rc_choose_account" type="id"/>
+ <item name="rc_get_accounts_permission" type="id"/>
+ <item name="rc_record_audio" type="id"/>
+</resources>
diff --git a/src/starboard/android/apk/app/src/main/res/values/strings.xml b/src/starboard/android/apk/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..8b7369f
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/res/values/strings.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2016 The Cobalt Authors. All Rights Reserved.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <!-- Error message when there's a problem connecting to the network. -->
+ <string name="starboard_platform_connection_error">Can\'t connect right now</string>
+ <!-- Button label on network connection error to retry the request. -->
+ <string name="starboard_platform_retry">Try again</string>
+ <!-- Button label on network connection error to open system network settings. -->
+ <string name="starboard_platform_network_settings">Open network settings</string>
+ <!-- Toast message when we can't get the device account to sign in. -->
+ <string name="starboard_account_picker_error">Account not available.</string>
+ <!-- Toast message when we the account can't be authorized. -->
+ <string name="starboard_account_auth_error">Account authorization failed.</string>
+ <!-- Toast message indicating that the selected account is no longer available to sign-in. -->
+ <string name="starboard_missing_account">Account not available:\n%1$s</string>
+ <!-- Toast message shown when the user has denied permission to get device accounts. -->
+ <string name="starboard_accounts_permission">Sign in requires Contacts permission to be granted in system settings.</string>
+</resources>
diff --git a/src/starboard/android/apk/app/src/main/res/values/styles.xml b/src/starboard/android/apk/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..ef05d47
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/res/values/styles.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2016 The Cobalt Authors. All Rights Reserved.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <style name="CobaltTheme" parent="@style/android:Theme.Material.NoActionBar">
+ <!-- Color of the transition animation when launching the app. -->
+ <item name="android:colorPrimary">@color/primary</item>
+
+ <!-- Color shown after the transition before the window is attached.
+ Avoids a black flash between the transition and splash screen. -->
+ <item name="android:windowBackground">?android:colorPrimary</item>
+
+ <item name="android:dialogTheme">@style/FullscreenDialogTheme</item>
+ </style>
+
+ <style name="FullscreenDialogTheme" parent="Theme.Leanback">
+ <item name="android:layout_width">fill_parent</item>
+ <item name="android:layout_height">fill_parent</item>
+ <item name="android:windowIsTranslucent">true</item>
+ <item name="android:windowBackground">@color/lb_error_background_color_translucent</item>
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:windowIsFloating">false</item>
+ <item name="android:windowOverscan">true</item>
+ <item name="android:backgroundDimEnabled">false</item>
+ <item name="errorMessageStyle">@style/ErrorMessageStyle</item>
+ <!-- Make the buttons darker so that you can see the focus highlight on the dark background. -->
+ <item name="android:colorButtonNormal">#282828</item>
+ </style>
+
+ <style name="ErrorMessageStyle" parent="Widget.Leanback.ErrorMessageStyle">
+ <item name="android:layout_margin">16dp</item>
+ </style>
+</resources>
diff --git a/src/starboard/android/apk/build.gradle b/src/starboard/android/apk/build.gradle
new file mode 100644
index 0000000..b50ae22
--- /dev/null
+++ b/src/starboard/android/apk/build.gradle
@@ -0,0 +1,55 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// 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.3.0'
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ }
+ gradle.projectsEvaluated {
+ tasks.withType(JavaCompile) {
+ options.compilerArgs += [
+ "-Xlint:unchecked",
+ "-Xlint:deprecation",
+ ]
+ }
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
+
+// Move the 'buildDir' for all projects into sub-directories of a shared top-level build directory,
+// which is either the root's original 'buildDir' or a custom location when building for platform
+// deploy. Note that the platform deploy action sets a custom 'cobaltGradleDir' property rather
+// than setting 'buildDir' directly on the command line since Gradle trys to get smart about
+// 'buildDir' which can end up putting it at the wrong depth in the file system.
+def rootBuildDir = hasProperty('cobaltGradleDir') ? new File(cobaltGradleDir, 'build') : buildDir
+allprojects { buildDir = new File(rootBuildDir, project.name).canonicalFile }
diff --git a/src/starboard/android/apk/cobalt-gradle.sh b/src/starboard/android/apk/cobalt-gradle.sh
new file mode 100755
index 0000000..d135b4b
--- /dev/null
+++ b/src/starboard/android/apk/cobalt-gradle.sh
@@ -0,0 +1,64 @@
+#!/bin/bash
+# Copyright 2016 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Helper to set ANDROID_HOME and ANDROID_NDK_HOME from command-line args
+# before running gradlew, as specified by leading --sdk and --ndk args.
+#
+# Also resets hung gradle builds when specified by a leading --reset arg.
+
+GRADLE_ARGS=()
+while [ "$1" ]; do
+ case "$1" in
+ --sdk) shift; ANDROID_HOME="$1" ;;
+ --ndk) shift; ANDROID_NDK_HOME="$1" ;;
+ --cache) shift; mkdir -p "$1";
+ GRADLE_ARGS+=("--project-cache-dir" $(cd "$1"; pwd)) ;;
+ --reset) RESET_GRADLE=1 ;;
+ *) break ;;
+ esac
+ shift
+done
+GRADLE_ARGS+=("$@")
+
+# Cleanup Gradle from previous builds. Used as part of the GYP step.
+if [[ "${RESET_GRADLE}" ]]; then
+ echo "Cleaning Gradle deamons and locks."
+ # If there are any lock files, kill any hung processes still waiting on them.
+ if compgen -G '/var/lock/cobalt-gradle.lock.*'; then
+ lsof -t /var/lock/cobalt-gradle.lock.* | xargs -rt kill
+ fi
+ # Stop the Gradle daemon (if still running).
+ $(dirname "$0")/gradlew --stop
+ # Remove Gradle caches (including its lock files).
+ rm -rf ${HOME}/.gradle/caches
+ # Show the gradle version, which will cause it to download if needed.
+ $(dirname "$0")/gradlew -v
+ # After resetting, exit without running any gradle tasks.
+ exit
+fi
+
+export ANDROID_HOME
+export ANDROID_NDK_HOME
+echo "ANDROID_HOME=${ANDROID_HOME}"
+echo "ANDROID_NDK_HOME=${ANDROID_NDK_HOME}"
+echo "TASK: ${GRADLE_ARGS[-1]}"
+
+# Allow parallel gradle builds, as defined by a COBALT_GRADLE_BUILD_COUNT envvar
+# or default to 1 if that's not set (so buildbot only runs 1 gradle at a time).
+BUCKETS=${COBALT_GRADLE_BUILD_COUNT:-1}
+MD5=$(echo "${GRADLE_ARGS[@]}" | md5sum)
+LOCKNUM=$(( ${BUCKETS} * 0x${MD5:0:6} / 0x1000000 ))
+
+flock /var/lock/cobalt-gradle.lock.${LOCKNUM} $(dirname "$0")/gradlew "${GRADLE_ARGS[@]}"
diff --git a/src/starboard/android/apk/gradle.properties b/src/starboard/android/apk/gradle.properties
new file mode 100644
index 0000000..2d5bfe3
--- /dev/null
+++ b/src/starboard/android/apk/gradle.properties
@@ -0,0 +1,31 @@
+# Copyright 2016 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# 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=-Xmx4g
+
+# 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/src/starboard/android/apk/gradle/wrapper/gradle-wrapper.jar b/src/starboard/android/apk/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..13372ae
--- /dev/null
+++ b/src/starboard/android/apk/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/src/starboard/android/apk/gradle/wrapper/gradle-wrapper.properties b/src/starboard/android/apk/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..b17c1fc
--- /dev/null
+++ b/src/starboard/android/apk/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue Jan 29 11:03:00 PST 2019
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip
diff --git a/src/starboard/android/apk/gradlew b/src/starboard/android/apk/gradlew
new file mode 100755
index 0000000..9d82f78
--- /dev/null
+++ b/src/starboard/android/apk/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/src/starboard/android/apk/gradlew.bat b/src/starboard/android/apk/gradlew.bat
new file mode 100644
index 0000000..aec9973
--- /dev/null
+++ b/src/starboard/android/apk/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/src/starboard/android/apk/settings.gradle b/src/starboard/android/apk/settings.gradle
new file mode 100644
index 0000000..1abb6c2
--- /dev/null
+++ b/src/starboard/android/apk/settings.gradle
@@ -0,0 +1,15 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+include ':app'
diff --git a/src/starboard/android/arm/atomic_public.h b/src/starboard/android/arm/atomic_public.h
new file mode 100644
index 0000000..57fad75
--- /dev/null
+++ b/src/starboard/android/arm/atomic_public.h
@@ -0,0 +1,15 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/atomic_public.h"
diff --git a/src/starboard/android/arm/configuration_public.h b/src/starboard/android/arm/configuration_public.h
new file mode 100644
index 0000000..aa26c16
--- /dev/null
+++ b/src/starboard/android/arm/configuration_public.h
@@ -0,0 +1,22 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// The Starboard configuration for Android ARM. Other devices will have
+// specific Starboard implementations, even if they ultimately are running some
+// version of Android.
+
+// Other source files should never include this header directly, but should
+// include the generic "starboard/configuration.h" instead.
+
+#include "starboard/android/shared/configuration_public.h"
diff --git a/src/starboard/android/arm/gyp_configuration.gypi b/src/starboard/android/arm/gyp_configuration.gypi
new file mode 100644
index 0000000..d3302b1
--- /dev/null
+++ b/src/starboard/android/arm/gyp_configuration.gypi
@@ -0,0 +1,53 @@
+# Copyright 2017 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+{
+ 'variables': {
+ 'target_arch': 'arm',
+ 'arm_version': 7,
+ 'armv7': 1,
+ 'arm_thumb': 0,
+ 'arm_neon': 0,
+ 'arm_fpu': 'vfpv3-d16',
+ 'compiler_flags': [
+ '-march=armv7-a',
+ ],
+ 'linker_flags': [
+ # Mimic build/cmake/android.toolchain.cmake in the Android NDK.
+ '-Wl,--exclude-libs,libunwind.a',
+ ]
+ },
+
+ 'target_defaults': {
+ 'default_configuration': 'android-arm_debug',
+ 'configurations': {
+ 'android-arm_debug': {
+ 'inherit_from': ['debug_base'],
+ },
+ 'android-arm_devel': {
+ 'inherit_from': ['devel_base'],
+ },
+ 'android-arm_qa': {
+ 'inherit_from': ['qa_base'],
+ },
+ 'android-arm_gold': {
+ 'inherit_from': ['gold_base'],
+ },
+ }, # end of configurations
+ },
+
+ 'includes': [
+ '../shared/gyp_configuration.gypi',
+ ],
+}
diff --git a/src/starboard/android/arm/gyp_configuration.py b/src/starboard/android/arm/gyp_configuration.py
new file mode 100644
index 0000000..b804d2f
--- /dev/null
+++ b/src/starboard/android/arm/gyp_configuration.py
@@ -0,0 +1,20 @@
+# Copyright 2017 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Starboard Android ARM platform build configuration."""
+
+from starboard.android.shared import gyp_configuration as shared_configuration
+
+
+def CreatePlatformConfig():
+ return shared_configuration.AndroidConfiguration('android-arm', 'armeabi-v7a')
diff --git a/src/starboard/android/arm/starboard_platform.gyp b/src/starboard/android/arm/starboard_platform.gyp
new file mode 100644
index 0000000..5110292
--- /dev/null
+++ b/src/starboard/android/arm/starboard_platform.gyp
@@ -0,0 +1,16 @@
+# Copyright 2017 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+{
+ 'includes': [ '../shared/starboard_platform.gypi' ],
+}
diff --git a/src/starboard/android/arm/starboard_platform_tests.gyp b/src/starboard/android/arm/starboard_platform_tests.gyp
new file mode 100644
index 0000000..58cd108
--- /dev/null
+++ b/src/starboard/android/arm/starboard_platform_tests.gyp
@@ -0,0 +1,16 @@
+# Copyright 2017 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+{
+ 'includes': [ '../shared/starboard_platform_tests.gypi' ],
+}
diff --git a/src/starboard/android/arm/thread_types_public.h b/src/starboard/android/arm/thread_types_public.h
new file mode 100644
index 0000000..0b84b59
--- /dev/null
+++ b/src/starboard/android/arm/thread_types_public.h
@@ -0,0 +1,15 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/thread_types_public.h"
diff --git a/src/starboard/android/arm64/atomic_public.h b/src/starboard/android/arm64/atomic_public.h
new file mode 100644
index 0000000..57fad75
--- /dev/null
+++ b/src/starboard/android/arm64/atomic_public.h
@@ -0,0 +1,15 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/atomic_public.h"
diff --git a/src/starboard/android/arm64/configuration_public.h b/src/starboard/android/arm64/configuration_public.h
new file mode 100644
index 0000000..52f31dc
--- /dev/null
+++ b/src/starboard/android/arm64/configuration_public.h
@@ -0,0 +1,22 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// The Starboard configuration for Android ARM-64. Other devices will have
+// specific Starboard implementations, even if they ultimately are running some
+// version of Android.
+
+// Other source files should never include this header directly, but should
+// include the generic "starboard/configuration.h" instead.
+
+#include "starboard/android/shared/configuration_public.h"
diff --git a/src/starboard/android/arm64/gyp_configuration.gypi b/src/starboard/android/arm64/gyp_configuration.gypi
new file mode 100644
index 0000000..cc3a2b5
--- /dev/null
+++ b/src/starboard/android/arm64/gyp_configuration.gypi
@@ -0,0 +1,50 @@
+# Copyright 2017 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+{
+ 'variables': {
+ 'target_arch': 'arm64',
+ 'arm_version': 8,
+ 'armv7': 0,
+ 'arm_thumb': 0,
+ 'arm_neon': 0,
+ 'arm_fpu': 'vfpv3-d16',
+ 'linker_flags': [
+ '-Wl,--no-keep-memory',
+ ],
+
+ },
+
+ 'target_defaults': {
+ 'default_configuration': 'android-arm64_debug',
+ 'configurations': {
+ 'android-arm64_debug': {
+ 'inherit_from': ['debug_base'],
+ },
+ 'android-arm64_devel': {
+ 'inherit_from': ['devel_base'],
+ },
+ 'android-arm64_qa': {
+ 'inherit_from': ['qa_base'],
+ },
+ 'android-arm64_gold': {
+ 'inherit_from': ['gold_base'],
+ },
+ }, # end of configurations
+ },
+
+ 'includes': [
+ '../shared/gyp_configuration.gypi',
+ ],
+}
diff --git a/src/starboard/android/arm64/gyp_configuration.py b/src/starboard/android/arm64/gyp_configuration.py
new file mode 100644
index 0000000..215dc50
--- /dev/null
+++ b/src/starboard/android/arm64/gyp_configuration.py
@@ -0,0 +1,20 @@
+# Copyright 2017 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Starboard Android ARM-64 platform build configuration."""
+
+from starboard.android.shared import gyp_configuration as shared_configuration
+
+
+def CreatePlatformConfig():
+ return shared_configuration.AndroidConfiguration('android-arm64', 'arm64-v8a')
diff --git a/src/starboard/android/arm64/starboard_platform.gyp b/src/starboard/android/arm64/starboard_platform.gyp
new file mode 100644
index 0000000..5110292
--- /dev/null
+++ b/src/starboard/android/arm64/starboard_platform.gyp
@@ -0,0 +1,16 @@
+# Copyright 2017 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+{
+ 'includes': [ '../shared/starboard_platform.gypi' ],
+}
diff --git a/src/starboard/android/arm64/starboard_platform_tests.gyp b/src/starboard/android/arm64/starboard_platform_tests.gyp
new file mode 100644
index 0000000..58cd108
--- /dev/null
+++ b/src/starboard/android/arm64/starboard_platform_tests.gyp
@@ -0,0 +1,16 @@
+# Copyright 2017 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+{
+ 'includes': [ '../shared/starboard_platform_tests.gypi' ],
+}
diff --git a/src/starboard/android/arm64/thread_types_public.h b/src/starboard/android/arm64/thread_types_public.h
new file mode 100644
index 0000000..0b84b59
--- /dev/null
+++ b/src/starboard/android/arm64/thread_types_public.h
@@ -0,0 +1,15 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/thread_types_public.h"
diff --git a/src/starboard/android/shared/__init__.py b/src/starboard/android/shared/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/starboard/android/shared/__init__.py
diff --git a/src/starboard/android/shared/accessibility_get_caption_settings.cc b/src/starboard/android/shared/accessibility_get_caption_settings.cc
new file mode 100644
index 0000000..58e3067
--- /dev/null
+++ b/src/starboard/android/shared/accessibility_get_caption_settings.cc
@@ -0,0 +1,322 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <cmath>
+#include <cstdlib>
+#include <limits>
+#include <string>
+
+#include "base/compiler_specific.h"
+#include "base/logging.h"
+
+#include "starboard/accessibility.h"
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/configuration.h"
+#include "starboard/log.h"
+#include "starboard/memory.h"
+
+using starboard::android::shared::JniEnvExt;
+using starboard::android::shared::ScopedLocalJavaRef;
+
+namespace {
+
+const int kRgbWhite = 0xFFFFFF;
+const int kRgbBlack = 0x000000;
+const int kRgbRed = 0xFF0000;
+const int kRgbYellow = 0xFFFF00;
+const int kRgbGreen = 0x00FF00;
+const int kRgbCyan = 0x00FFFF;
+const int kRgbBlue = 0x0000FF;
+const int kRgbMagenta = 0xFF00FF;
+
+const int kRgbColors[] = {
+ kRgbWhite,
+ kRgbBlack,
+ kRgbRed,
+ kRgbYellow,
+ kRgbGreen,
+ kRgbCyan,
+ kRgbBlue,
+ kRgbMagenta,
+};
+
+SbAccessibilityCaptionColor GetClosestCaptionColor(int color) {
+ int ref_color = kRgbWhite;
+ int min_distance = std::numeric_limits<int>::max();
+
+ int r = 0xFF & (color >> 16);
+ int g = 0xFF & (color >> 8);
+ int b = 0xFF & (color);
+
+ // Find the reference color with the least distance (squared).
+ for (int i = 0; i < SB_ARRAY_SIZE(kRgbColors); i++) {
+ int r_ref = 0xFF & (kRgbColors[i] >> 16);
+ int g_ref = 0xFF & (kRgbColors[i] >> 8);
+ int b_ref = 0xFF & (kRgbColors[i]);
+ int distance_squared = pow(r - r_ref, 2) +
+ pow(g - g_ref, 2) +
+ pow(b - b_ref, 2);
+ if (distance_squared < min_distance) {
+ ref_color = kRgbColors[i];
+ min_distance = distance_squared;
+ }
+ }
+
+ switch (ref_color) {
+ case kRgbWhite:
+ return kSbAccessibilityCaptionColorWhite;
+ case kRgbBlack:
+ return kSbAccessibilityCaptionColorBlack;
+ case kRgbRed:
+ return kSbAccessibilityCaptionColorRed;
+ case kRgbYellow:
+ return kSbAccessibilityCaptionColorYellow;
+ case kRgbGreen:
+ return kSbAccessibilityCaptionColorGreen;
+ case kRgbCyan:
+ return kSbAccessibilityCaptionColorCyan;
+ case kRgbBlue:
+ return kSbAccessibilityCaptionColorBlue;
+ case kRgbMagenta:
+ return kSbAccessibilityCaptionColorMagenta;
+ default:
+ NOTREACHED() << "Invalid RGB color conversion";
+ return kSbAccessibilityCaptionColorWhite;
+ }
+}
+
+SbAccessibilityCaptionCharacterEdgeStyle
+AndroidEdgeTypeToSbEdgeStyle(int edge_type) {
+ switch (edge_type) {
+ case 0:
+ return kSbAccessibilityCaptionCharacterEdgeStyleNone;
+ case 1:
+ return kSbAccessibilityCaptionCharacterEdgeStyleUniform;
+ case 2:
+ return kSbAccessibilityCaptionCharacterEdgeStyleDropShadow;
+ case 3:
+ return kSbAccessibilityCaptionCharacterEdgeStyleRaised;
+ case 4:
+ return kSbAccessibilityCaptionCharacterEdgeStyleDepressed;
+ default:
+ NOTREACHED() << "Invalid edge type conversion";
+ return kSbAccessibilityCaptionCharacterEdgeStyleNone;
+ }
+}
+
+SbAccessibilityCaptionFontFamily AndroidFontFamilyToSbFontFamily(int family) {
+ switch (family) {
+ case 0:
+ return kSbAccessibilityCaptionFontFamilyCasual;
+ case 1:
+ return kSbAccessibilityCaptionFontFamilyCursive;
+ case 2:
+ return kSbAccessibilityCaptionFontFamilyMonospaceSansSerif;
+ case 3:
+ return kSbAccessibilityCaptionFontFamilyMonospaceSerif;
+ case 4:
+ return kSbAccessibilityCaptionFontFamilyProportionalSansSerif;
+ case 5:
+ return kSbAccessibilityCaptionFontFamilyProportionalSerif;
+ case 6:
+ return kSbAccessibilityCaptionFontFamilySmallCapitals;
+ default:
+ NOTREACHED() << "Invalid font family conversion";
+ return kSbAccessibilityCaptionFontFamilyCasual;
+ }
+}
+
+int FindClosestReferenceValue(int value, const int reference[],
+ size_t reference_size) {
+ int result = reference[0];
+ int min_difference = std::numeric_limits<int>::max();
+
+ for (int i = 0; i < reference_size; i++) {
+ int difference = abs(reference[i] - value);
+ if (difference < min_difference) {
+ result = reference[i];
+ min_difference = difference;
+ }
+ }
+ return result;
+}
+
+const int kFontSizes[] = {
+ 25,
+ 50,
+ 75,
+ 100,
+ 125,
+ 150,
+ 175,
+ 200,
+ 225,
+ 250,
+ 275,
+ 300
+};
+
+SbAccessibilityCaptionFontSizePercentage GetClosestFontSizePercentage(
+ int font_size_percent) {
+ int reference_size = FindClosestReferenceValue(
+ font_size_percent, kFontSizes, SB_ARRAY_SIZE(kFontSizes));
+ switch (reference_size) {
+ case 25:
+ return kSbAccessibilityCaptionFontSizePercentage25;
+ case 50:
+ return kSbAccessibilityCaptionFontSizePercentage50;
+ case 75:
+ return kSbAccessibilityCaptionFontSizePercentage75;
+ case 100:
+ return kSbAccessibilityCaptionFontSizePercentage100;
+ case 125:
+ return kSbAccessibilityCaptionFontSizePercentage125;
+ case 150:
+ return kSbAccessibilityCaptionFontSizePercentage150;
+ case 175:
+ return kSbAccessibilityCaptionFontSizePercentage175;
+ case 200:
+ return kSbAccessibilityCaptionFontSizePercentage200;
+ case 225:
+ return kSbAccessibilityCaptionFontSizePercentage225;
+ case 250:
+ return kSbAccessibilityCaptionFontSizePercentage250;
+ case 275:
+ return kSbAccessibilityCaptionFontSizePercentage275;
+ case 300:
+ return kSbAccessibilityCaptionFontSizePercentage300;
+ default:
+ NOTREACHED() << "Invalid font size";
+ return kSbAccessibilityCaptionFontSizePercentage100;
+ }
+}
+
+const int kOpacities[] = {
+ 0,
+ 25,
+ 50,
+ 75,
+ 100,
+};
+
+SbAccessibilityCaptionOpacityPercentage GetClosestOpacity(int opacity_percent) {
+ int reference_opacity_percent = FindClosestReferenceValue(
+ opacity_percent, kOpacities, SB_ARRAY_SIZE(kOpacities));
+ switch (reference_opacity_percent) {
+ case 0:
+ return kSbAccessibilityCaptionOpacityPercentage0;
+ case 25:
+ return kSbAccessibilityCaptionOpacityPercentage25;
+ case 50:
+ return kSbAccessibilityCaptionOpacityPercentage50;
+ case 75:
+ return kSbAccessibilityCaptionOpacityPercentage75;
+ case 100:
+ return kSbAccessibilityCaptionOpacityPercentage100;
+ default:
+ NOTREACHED() << "Invalid opacity percentage";
+ return kSbAccessibilityCaptionOpacityPercentage100;
+ }
+}
+
+SbAccessibilityCaptionState BooleanToCaptionState(bool is_set) {
+ if (is_set) {
+ return kSbAccessibilityCaptionStateSet;
+ } else {
+ return kSbAccessibilityCaptionStateUnset;
+ }
+}
+
+void SetColorProperties(jobject j_caption_settings,
+ const char* color_field,
+ const char* has_color_field,
+ SbAccessibilityCaptionColor* color,
+ SbAccessibilityCaptionState* color_state,
+ SbAccessibilityCaptionOpacityPercentage* opacity,
+ SbAccessibilityCaptionState* opacity_state) {
+ JniEnvExt* env = JniEnvExt::Get();
+ jint j_color = env->GetIntFieldOrAbort(j_caption_settings, color_field, "I");
+ *color = GetClosestCaptionColor(j_color);
+ *opacity = GetClosestOpacity((0xFF & (j_color >> 24)) * 100 / 255);
+ *color_state = BooleanToCaptionState(
+ env->GetBooleanFieldOrAbort(j_caption_settings, has_color_field, "Z"));
+ // Color and opacity are combined into a single ARGB value.
+ // Therefore, if the color is set, so is the opacity.
+ *opacity_state = *color_state;
+}
+
+} // namespace
+
+bool SbAccessibilityGetCaptionSettings(
+ SbAccessibilityCaptionSettings* caption_settings) {
+ if (!caption_settings ||
+ !SbMemoryIsZero(caption_settings,
+ sizeof(SbAccessibilityCaptionSettings))) {
+ return false;
+ }
+
+ JniEnvExt* env = JniEnvExt::Get();
+
+ ScopedLocalJavaRef<jobject> j_caption_settings(
+ env->CallStarboardObjectMethodOrAbort(
+ "getCaptionSettings", "()Ldev/cobalt/media/CaptionSettings;"));
+
+ jfloat font_scale =
+ env->GetFloatFieldOrAbort(j_caption_settings.Get(), "fontScale", "F");
+ caption_settings->font_size =
+ GetClosestFontSizePercentage(100.0 * font_scale);
+ // Android's captioning API always returns a font scale of 1 (100%) if
+ // the font size has not been set. This means we have no way to check if
+ // font size is "set" vs. "unset", so we'll just return "set" every time.
+ caption_settings->font_size_state = kSbAccessibilityCaptionStateSet;
+
+ // TODO: Convert Android typeface to font family.
+ caption_settings->font_family = kSbAccessibilityCaptionFontFamilyCasual;
+ caption_settings->font_family_state = kSbAccessibilityCaptionStateUnsupported;
+
+ caption_settings->character_edge_style = AndroidEdgeTypeToSbEdgeStyle(
+ env->GetIntFieldOrAbort(j_caption_settings.Get(), "edgeType", "I"));
+ caption_settings->character_edge_style_state = BooleanToCaptionState(
+ env->GetBooleanFieldOrAbort(j_caption_settings.Get(),
+ "hasEdgeType", "Z"));
+
+ SetColorProperties(
+ j_caption_settings.Get(), "foregroundColor", "hasForegroundColor",
+ &caption_settings->font_color,
+ &caption_settings->font_color_state,
+ &caption_settings->font_opacity,
+ &caption_settings->font_opacity_state);
+
+ SetColorProperties(
+ j_caption_settings.Get(), "backgroundColor", "hasBackgroundColor",
+ &caption_settings->background_color,
+ &caption_settings->background_color_state,
+ &caption_settings->background_opacity,
+ &caption_settings->background_opacity_state);
+
+ SetColorProperties(
+ j_caption_settings.Get(), "windowColor", "hasWindowColor",
+ &caption_settings->window_color,
+ &caption_settings->window_color_state,
+ &caption_settings->window_opacity,
+ &caption_settings->window_opacity_state);
+
+ caption_settings->is_enabled =
+ env->GetBooleanFieldOrAbort(j_caption_settings.Get(), "isEnabled", "Z");
+ caption_settings->supports_is_enabled = true;
+ caption_settings->supports_set_enabled = false;
+
+ return true;
+}
diff --git a/src/starboard/android/shared/accessibility_get_display_settings.cc b/src/starboard/android/shared/accessibility_get_display_settings.cc
new file mode 100644
index 0000000..d3d10e7
--- /dev/null
+++ b/src/starboard/android/shared/accessibility_get_display_settings.cc
@@ -0,0 +1,37 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/accessibility.h"
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/memory.h"
+
+using starboard::android::shared::JniEnvExt;
+
+bool SbAccessibilityGetDisplaySettings(
+ SbAccessibilityDisplaySettings* out_setting) {
+ if (!out_setting ||
+ !SbMemoryIsZero(out_setting,
+ sizeof(SbAccessibilityDisplaySettings))) {
+ return false;
+ }
+
+ JniEnvExt* env = JniEnvExt::Get();
+ out_setting->has_high_contrast_text_setting = true;
+ out_setting->is_high_contrast_text_enabled =
+ env->CallStarboardBooleanMethodOrAbort(
+ "isAccessibilityHighContrastTextEnabled", "()Z");
+
+ return true;
+}
diff --git a/src/starboard/android/shared/accessibility_get_text_to_speech_settings.cc b/src/starboard/android/shared/accessibility_get_text_to_speech_settings.cc
new file mode 100644
index 0000000..8f64452
--- /dev/null
+++ b/src/starboard/android/shared/accessibility_get_text_to_speech_settings.cc
@@ -0,0 +1,44 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/accessibility.h"
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/memory.h"
+
+using starboard::android::shared::JniEnvExt;
+using starboard::android::shared::ScopedLocalJavaRef;
+
+bool SbAccessibilityGetTextToSpeechSettings(
+ SbAccessibilityTextToSpeechSettings* out_setting) {
+ if (!out_setting ||
+ !SbMemoryIsZero(out_setting,
+ sizeof(SbAccessibilityTextToSpeechSettings))) {
+ return false;
+ }
+
+ JniEnvExt* env = JniEnvExt::Get();
+
+ out_setting->has_text_to_speech_setting = true;
+ ScopedLocalJavaRef<jobject> j_tts_helper(
+ env->CallStarboardObjectMethodOrAbort(
+ "getTextToSpeechHelper",
+ "()Ldev/cobalt/coat/CobaltTextToSpeechHelper;"));
+ out_setting->is_text_to_speech_enabled =
+ env->CallBooleanMethodOrAbort(j_tts_helper.Get(),
+ "isScreenReaderEnabled", "()Z");
+
+ return true;
+}
diff --git a/src/starboard/android/shared/accessibility_set_captions_enabled.cc b/src/starboard/android/shared/accessibility_set_captions_enabled.cc
new file mode 100644
index 0000000..f39ad20
--- /dev/null
+++ b/src/starboard/android/shared/accessibility_set_captions_enabled.cc
@@ -0,0 +1,21 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/accessibility.h"
+#include "starboard/configuration.h"
+
+bool SbAccessibilitySetCaptionsEnabled(bool enabled) {
+ SB_UNREFERENCED_PARAMETER(enabled);
+ return false;
+}
diff --git a/src/starboard/android/shared/android_main.cc b/src/starboard/android/shared/android_main.cc
new file mode 100644
index 0000000..771ad25
--- /dev/null
+++ b/src/starboard/android/shared/android_main.cc
@@ -0,0 +1,214 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/application_android.h"
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/android/shared/log_internal.h"
+#include "starboard/common/semaphore.h"
+#include "starboard/shared/starboard/command_line.h"
+#include "starboard/string.h"
+#include "starboard/thread.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+namespace {
+
+using ::starboard::shared::starboard::CommandLine;
+typedef ::starboard::android::shared::ApplicationAndroid::AndroidCommand
+ AndroidCommand;
+
+SbThread g_starboard_thread = kSbThreadInvalid;
+
+// Safeguard to avoid sending AndroidCommands either when there is no instance
+// of the Starboard application, or after the run loop has exited and the
+// ALooper receiving the commands is no longer being polled.
+bool g_app_running = false;
+
+std::vector<std::string> GetArgs() {
+ std::vector<std::string> args;
+ // Fake program name as args[0]
+ args.push_back(SbStringDuplicate("android_main"));
+
+ JniEnvExt* env = JniEnvExt::Get();
+
+ ScopedLocalJavaRef<jobjectArray> args_array(
+ env->CallStarboardObjectMethodOrAbort("getArgs",
+ "()[Ljava/lang/String;"));
+ jint argc = !args_array ? 0 : env->GetArrayLength(args_array.Get());
+
+ for (jint i = 0; i < argc; i++) {
+ ScopedLocalJavaRef<jstring> element(
+ env->GetObjectArrayElementOrAbort(args_array.Get(), i));
+ args.push_back(env->GetStringStandardUTFOrAbort(element.Get()));
+ }
+
+ return args;
+}
+
+std::string GetStartDeepLink() {
+ JniEnvExt* env = JniEnvExt::Get();
+ std::string start_url;
+
+ ScopedLocalJavaRef<jstring> j_url(env->CallStarboardObjectMethodOrAbort(
+ "getStartDeepLink", "()Ljava/lang/String;"));
+ if (j_url) {
+ start_url = env->GetStringStandardUTFOrAbort(j_url.Get());
+ }
+ return start_url;
+}
+
+void* ThreadEntryPoint(void* context) {
+ Semaphore* app_created_semaphore = static_cast<Semaphore*>(context);
+
+ ALooper* looper = ALooper_prepare(ALOOPER_PREPARE_ALLOW_NON_CALLBACKS);
+ ApplicationAndroid app(looper);
+
+ CommandLine command_line(GetArgs());
+ LogInit(command_line);
+
+ // Mark the app running before signaling app created so there's no race to
+ // allow sending the first AndroidCommand after onCreate() returns.
+ g_app_running = true;
+
+ // Signal ANativeActivity_onCreate() that it may proceed.
+ app_created_semaphore->Put();
+
+ // Enter the Starboard run loop until stopped.
+ int error_level =
+ app.Run(std::move(command_line), GetStartDeepLink().c_str());
+
+ // Mark the app not running before informing StarboardBridge that the app is
+ // stopped so that we won't send any more AndroidCommands as a result of
+ // shutting down the Activity.
+ g_app_running = false;
+
+ // Our launcher.py looks for this to know when the app (test) is done.
+ SB_LOG(INFO) << "***Application Stopped*** " << error_level;
+
+ // Inform StarboardBridge that the run loop has exited so it can cleanup and
+ // kill the process.
+ JniEnvExt* env = JniEnvExt::Get();
+ env->CallStarboardVoidMethodOrAbort("afterStopped", "()V");
+
+ return NULL;
+}
+
+void OnStart(ANativeActivity* activity) {
+ if (g_app_running) {
+ ApplicationAndroid::Get()->SendAndroidCommand(AndroidCommand::kStart);
+ }
+}
+
+void OnResume(ANativeActivity* activity) {
+ if (g_app_running) {
+ ApplicationAndroid::Get()->SendAndroidCommand(AndroidCommand::kResume);
+ }
+}
+
+void OnPause(ANativeActivity* activity) {
+ if (g_app_running) {
+ ApplicationAndroid::Get()->SendAndroidCommand(AndroidCommand::kPause);
+ }
+}
+
+void OnStop(ANativeActivity* activity) {
+ if (g_app_running) {
+ ApplicationAndroid::Get()->SendAndroidCommand(AndroidCommand::kStop);
+ }
+}
+
+void OnWindowFocusChanged(ANativeActivity* activity, int focused) {
+ if (g_app_running) {
+ ApplicationAndroid::Get()->SendAndroidCommand(focused
+ ? AndroidCommand::kWindowFocusGained
+ : AndroidCommand::kWindowFocusLost);
+ }
+}
+
+void OnNativeWindowCreated(ANativeActivity* activity, ANativeWindow* window) {
+ if (g_app_running) {
+ ApplicationAndroid::Get()->SendAndroidCommand(
+ AndroidCommand::kNativeWindowCreated, window);
+ }
+}
+
+void OnNativeWindowDestroyed(ANativeActivity* activity, ANativeWindow* window) {
+ if (g_app_running) {
+ ApplicationAndroid::Get()->SendAndroidCommand(
+ AndroidCommand::kNativeWindowDestroyed);
+ }
+}
+
+void OnInputQueueCreated(ANativeActivity* activity, AInputQueue* queue) {
+ if (g_app_running) {
+ ApplicationAndroid::Get()->SendAndroidCommand(
+ AndroidCommand::kInputQueueChanged, queue);
+ }
+}
+
+void OnInputQueueDestroyed(ANativeActivity* activity, AInputQueue* queue) {
+ if (g_app_running) {
+ ApplicationAndroid::Get()->SendAndroidCommand(
+ AndroidCommand::kInputQueueChanged, NULL);
+ }
+}
+
+extern "C" SB_EXPORT_PLATFORM void ANativeActivity_onCreate(
+ ANativeActivity *activity, void *savedState, size_t savedStateSize) {
+
+ // Start the Starboard thread the first time an Activity is created.
+ if (!SbThreadIsValid(g_starboard_thread)) {
+ Semaphore semaphore;
+
+ g_starboard_thread = SbThreadCreate(
+ 0, kSbThreadPriorityNormal, kSbThreadNoAffinity, false,
+ "StarboardMain", &ThreadEntryPoint, &semaphore);
+
+ // Wait for the ApplicationAndroid to be created.
+ semaphore.Take();
+ }
+
+ activity->callbacks->onStart = OnStart;
+ activity->callbacks->onResume = OnResume;
+ activity->callbacks->onPause = OnPause;
+ activity->callbacks->onStop = OnStop;
+ activity->callbacks->onWindowFocusChanged = OnWindowFocusChanged;
+ activity->callbacks->onNativeWindowCreated = OnNativeWindowCreated;
+ activity->callbacks->onNativeWindowDestroyed = OnNativeWindowDestroyed;
+ activity->callbacks->onInputQueueCreated = OnInputQueueCreated;
+ activity->callbacks->onInputQueueDestroyed = OnInputQueueDestroyed;
+ activity->instance = ApplicationAndroid::Get();
+}
+
+extern "C" SB_EXPORT_PLATFORM
+jboolean Java_dev_cobalt_coat_StarboardBridge_nativeIsReleaseBuild() {
+#if defined(COBALT_BUILD_TYPE_GOLD)
+ return true;
+#else
+ return false;
+#endif
+}
+
+extern "C" SB_EXPORT_PLATFORM
+void Java_dev_cobalt_coat_StarboardBridge_nativeInitialize(
+ JniEnvExt* env, jobject starboard_bridge) {
+ JniEnvExt::Initialize(env, starboard_bridge);
+}
+
+} // namespace
+} // namespace shared
+} // namespace android
+} // namespace starboard
diff --git a/src/starboard/android/shared/application_android.cc b/src/starboard/android/shared/application_android.cc
new file mode 100644
index 0000000..8e8b2dc
--- /dev/null
+++ b/src/starboard/android/shared/application_android.cc
@@ -0,0 +1,500 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/application_android.h"
+
+#include <android/looper.h>
+#include <android/native_activity.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <string>
+#include <vector>
+
+#include "starboard/accessibility.h"
+#include "starboard/android/shared/file_internal.h"
+#include "starboard/android/shared/input_events_generator.h"
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/android/shared/window_internal.h"
+#include "starboard/condition_variable.h"
+#include "starboard/event.h"
+#include "starboard/log.h"
+#include "starboard/mutex.h"
+#include "starboard/shared/starboard/audio_sink/audio_sink_internal.h"
+#include "starboard/string.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+namespace {
+ enum {
+ kLooperIdAndroidCommand,
+ kLooperIdAndroidInput,
+ kLooperIdKeyboardInject,
+ };
+
+ const char* AndroidCommandName(
+ ApplicationAndroid::AndroidCommand::CommandType type) {
+ switch (type) {
+ case ApplicationAndroid::AndroidCommand::kUndefined:
+ return "Undefined";
+ case ApplicationAndroid::AndroidCommand::kStart:
+ return "Start";
+ case ApplicationAndroid::AndroidCommand::kResume:
+ return "Resume";
+ case ApplicationAndroid::AndroidCommand::kPause:
+ return "Pause";
+ case ApplicationAndroid::AndroidCommand::kStop:
+ return "Stop";
+ case ApplicationAndroid::AndroidCommand::kInputQueueChanged:
+ return "InputQueueChanged";
+ case ApplicationAndroid::AndroidCommand::kNativeWindowCreated:
+ return "NativeWindowCreated";
+ case ApplicationAndroid::AndroidCommand::kNativeWindowDestroyed:
+ return "NativeWindowDestroyed";
+ case ApplicationAndroid::AndroidCommand::kWindowFocusGained:
+ return "WindowFocusGained";
+ case ApplicationAndroid::AndroidCommand::kWindowFocusLost:
+ return "WindowFocusLost";
+ default:
+ return "unknown";
+ }
+ }
+} // namespace
+
+// "using" doesn't work with class members, so make a local convenience type.
+typedef ::starboard::shared::starboard::Application::Event Event;
+
+ApplicationAndroid::ApplicationAndroid(ALooper* looper)
+ : looper_(looper),
+ native_window_(NULL),
+ input_queue_(NULL),
+ android_command_readfd_(-1),
+ android_command_writefd_(-1),
+ keyboard_inject_readfd_(-1),
+ keyboard_inject_writefd_(-1),
+ android_command_condition_(android_command_mutex_),
+ activity_state_(AndroidCommand::kUndefined),
+ window_(kSbWindowInvalid),
+ last_is_accessibility_high_contrast_text_enabled_(false) {
+
+ // Initialize Time Zone early so that local time works correctly.
+ // Called once here to help SbTimeZoneGet*Name()
+ tzset();
+
+ // Initialize Android asset access early so that ICU can load its tables
+ // from the assets. The use ICU is used in our logging.
+ SbFileAndroidInitialize();
+
+ int pipefd[2];
+ int err;
+
+ err = pipe(pipefd);
+ SB_CHECK(err >= 0) << "pipe errno is:" << errno;
+ android_command_readfd_ = pipefd[0];
+ android_command_writefd_ = pipefd[1];
+ ALooper_addFd(looper_, android_command_readfd_, kLooperIdAndroidCommand,
+ ALOOPER_EVENT_INPUT, NULL, NULL);
+
+ err = pipe(pipefd);
+ SB_CHECK(err >= 0) << "pipe errno is:" << errno;
+ keyboard_inject_readfd_ = pipefd[0];
+ keyboard_inject_writefd_ = pipefd[1];
+ ALooper_addFd(looper_, keyboard_inject_readfd_, kLooperIdKeyboardInject,
+ ALOOPER_EVENT_INPUT, NULL, NULL);
+}
+
+ApplicationAndroid::~ApplicationAndroid() {
+ ALooper_removeFd(looper_, android_command_readfd_);
+ close(android_command_readfd_);
+ close(android_command_writefd_);
+
+ ALooper_removeFd(looper_, keyboard_inject_readfd_);
+ close(keyboard_inject_readfd_);
+ close(keyboard_inject_writefd_);
+}
+
+void ApplicationAndroid::Initialize() {
+ SbAudioSinkPrivate::Initialize();
+}
+
+void ApplicationAndroid::Teardown() {
+ SbAudioSinkPrivate::TearDown();
+ SbFileAndroidTeardown();
+}
+
+SbWindow ApplicationAndroid::CreateWindow(const SbWindowOptions* options) {
+ SB_UNREFERENCED_PARAMETER(options);
+ if (SbWindowIsValid(window_)) {
+ return kSbWindowInvalid;
+ }
+ window_ = new SbWindowPrivate;
+ window_->native_window = native_window_;
+ input_events_generator_.reset(new InputEventsGenerator(window_));
+ return window_;
+}
+
+bool ApplicationAndroid::DestroyWindow(SbWindow window) {
+ if (!SbWindowIsValid(window)) {
+ return false;
+ }
+
+ input_events_generator_.reset();
+
+ SB_DCHECK(window == window_);
+ delete window_;
+ window_ = kSbWindowInvalid;
+ return true;
+}
+
+Event* ApplicationAndroid::WaitForSystemEventWithTimeout(SbTime time) {
+ // Convert from microseconds to milliseconds, taking the ceiling value.
+ // If we take the floor, or round, then we end up busy looping every time
+ // the next event time is less than one millisecond.
+ int timeout_millis = (time + kSbTimeMillisecond - 1) / kSbTimeMillisecond;
+ int looper_events;
+ int ident = ALooper_pollAll(timeout_millis, NULL, &looper_events, NULL);
+ switch (ident) {
+ case kLooperIdAndroidCommand:
+ ProcessAndroidCommand();
+ break;
+ case kLooperIdAndroidInput:
+ ProcessAndroidInput();
+ break;
+ case kLooperIdKeyboardInject:
+ ProcessKeyboardInject();
+ break;
+ }
+
+ // Always return NULL since we already dispatched our own system events.
+ return NULL;
+}
+
+void ApplicationAndroid::WakeSystemEventWait() {
+ ALooper_wake(looper_);
+}
+
+void ApplicationAndroid::OnResume() {
+ JniEnvExt* env = JniEnvExt::Get();
+ env->CallStarboardVoidMethodOrAbort("beforeStartOrResume", "()V");
+}
+
+void ApplicationAndroid::ProcessAndroidCommand() {
+ JniEnvExt* env = JniEnvExt::Get();
+ AndroidCommand cmd;
+ int err = read(android_command_readfd_, &cmd, sizeof(cmd));
+ SB_DCHECK(err >= 0) << "Command read failed. errno=" << errno;
+
+ SB_LOG(INFO) << "Android command: " << AndroidCommandName(cmd.type);
+
+ // The activity state to which we should sync the starboard state.
+ AndroidCommand::CommandType sync_state = AndroidCommand::kUndefined;
+
+ switch (cmd.type) {
+ case AndroidCommand::kUndefined:
+ break;
+
+ case AndroidCommand::kInputQueueChanged: {
+ ScopedLock lock(android_command_mutex_);
+ if (input_queue_) {
+ AInputQueue_detachLooper(input_queue_);
+ }
+ input_queue_ = static_cast<AInputQueue*>(cmd.data);
+ if (input_queue_) {
+ AInputQueue_attachLooper(input_queue_, looper_, kLooperIdAndroidInput,
+ NULL, NULL);
+ }
+ // Now that we've swapped our use of the input queue, signal that the
+ // Android UI thread can continue.
+ android_command_condition_.Signal();
+ break;
+ }
+
+ // Starboard resume/suspend is tied to the UI window being created/destroyed
+ // (rather than to the Activity lifecycle) since Cobalt can't do anything at
+ // all if it doesn't have a window surface to draw on.
+ case AndroidCommand::kNativeWindowCreated:
+ {
+ ScopedLock lock(android_command_mutex_);
+ native_window_ = static_cast<ANativeWindow*>(cmd.data);
+ if (window_) {
+ window_->native_window = native_window_;
+ }
+ // Now that we have the window, signal that the Android UI thread can
+ // continue, before we start or resume the Starboard app.
+ android_command_condition_.Signal();
+ }
+ if (state() == kStateUnstarted) {
+ // This is the initial launch, so we have to start Cobalt now that we
+ // have a window.
+ env->CallStarboardVoidMethodOrAbort("beforeStartOrResume", "()V");
+ DispatchStart();
+ } else {
+ // Now that we got a window back, change the command for the switch
+ // below to sync up with the current activity lifecycle.
+ sync_state = activity_state_;
+ }
+ break;
+ case AndroidCommand::kNativeWindowDestroyed:
+ env->CallStarboardVoidMethodOrAbort("beforeSuspend", "()V");
+ {
+ ScopedLock lock(android_command_mutex_);
+ // Cobalt can't keep running without a window, even if the Activity
+ // hasn't stopped yet. DispatchAndDelete() will inject events as needed
+ // if we're not already paused.
+ DispatchAndDelete(new Event(kSbEventTypeSuspend, NULL, NULL));
+ if (window_) {
+ window_->native_window = NULL;
+ }
+ native_window_ = NULL;
+ // Now that we've suspended the Starboard app, and let go of the window,
+ // signal that the Android UI thread can continue.
+ android_command_condition_.Signal();
+ }
+ break;
+
+ case AndroidCommand::kWindowFocusLost:
+ break;
+ case AndroidCommand::kWindowFocusGained: {
+ // Android does not have a publicly-exposed way to
+ // register for high-contrast text settings changed events.
+ // We assume that it can only change when our focus changes
+ // (because the user exits and enters the app) so we check
+ // for changes here.
+ SbAccessibilityDisplaySettings settings;
+ SbMemorySet(&settings, 0, sizeof(settings));
+ if (!SbAccessibilityGetDisplaySettings(&settings)) {
+ break;
+ }
+
+ bool enabled = settings.has_high_contrast_text_setting &&
+ settings.is_high_contrast_text_enabled;
+
+ if (enabled != last_is_accessibility_high_contrast_text_enabled_) {
+ DispatchAndDelete(new Event(
+ kSbEventTypeAccessiblitySettingsChanged, NULL, NULL));
+ }
+ last_is_accessibility_high_contrast_text_enabled_ = enabled;
+ break;
+ }
+
+ // Remember the Android activity state to sync to when we have a window.
+ case AndroidCommand::kStart:
+ case AndroidCommand::kResume:
+ case AndroidCommand::kPause:
+ case AndroidCommand::kStop:
+ sync_state = activity_state_ = cmd.type;
+ break;
+ }
+
+ // If there's a window, sync the app state to the Activity lifecycle, letting
+ // DispatchAndDelete() inject events as needed if we missed a state.
+ if (native_window_) {
+ switch (sync_state) {
+ case AndroidCommand::kStart:
+ DispatchAndDelete(new Event(kSbEventTypeResume, NULL, NULL));
+ break;
+ case AndroidCommand::kResume:
+ DispatchAndDelete(new Event(kSbEventTypeUnpause, NULL, NULL));
+ break;
+ case AndroidCommand::kPause:
+ DispatchAndDelete(new Event(kSbEventTypePause, NULL, NULL));
+ break;
+ case AndroidCommand::kStop:
+ if (state() != kStateSuspended) {
+ // We usually suspend when losing the window above, but if the window
+ // wasn't destroyed (e.g. when Daydream starts) then we still have to
+ // suspend when the Activity is stopped.
+ env->CallStarboardVoidMethodOrAbort("beforeSuspend", "()V");
+ DispatchAndDelete(new Event(kSbEventTypeSuspend, NULL, NULL));
+ }
+ break;
+ default:
+ break;
+ }
+ }
+}
+
+void ApplicationAndroid::SendAndroidCommand(AndroidCommand::CommandType type,
+ void* data) {
+ SB_LOG(INFO) << "Send Android command: " << AndroidCommandName(type);
+ AndroidCommand cmd {type, data};
+ ScopedLock lock(android_command_mutex_);
+ write(android_command_writefd_, &cmd, sizeof(cmd));
+ // Synchronization only necessary when managing resources.
+ switch (type) {
+ case AndroidCommand::kInputQueueChanged:
+ while (input_queue_ != data) {
+ android_command_condition_.Wait();
+ }
+ break;
+ case AndroidCommand::kNativeWindowCreated:
+ case AndroidCommand::kNativeWindowDestroyed:
+ while (native_window_ != data) {
+ android_command_condition_.Wait();
+ }
+ break;
+ default:
+ break;
+ }
+}
+
+void ApplicationAndroid::ProcessAndroidInput() {
+ SB_DCHECK(input_events_generator_);
+ AInputEvent* android_event = NULL;
+ while (AInputQueue_getEvent(input_queue_, &android_event) >= 0) {
+ SB_LOG(INFO) << "Android input: type="
+ << AInputEvent_getType(android_event);
+ if (AInputQueue_preDispatchEvent(input_queue_, android_event)) {
+ continue;
+ }
+ InputEventsGenerator::Events app_events;
+ bool handled = input_events_generator_->CreateInputEventsFromAndroidEvent(
+ android_event, &app_events);
+ for (int i = 0; i < app_events.size(); ++i) {
+ DispatchAndDelete(app_events[i].release());
+ }
+ AInputQueue_finishEvent(input_queue_, android_event, handled);
+ }
+}
+
+void ApplicationAndroid::ProcessKeyboardInject() {
+ SbKey key;
+ int err = read(keyboard_inject_readfd_, &key, sizeof(key));
+ SB_DCHECK(err >= 0) << "Keyboard inject read failed: errno=" << errno;
+ SB_LOG(INFO) << "Keyboard inject: " << key;
+
+ InputEventsGenerator::Events app_events;
+ input_events_generator_->CreateInputEventsFromSbKey(key, &app_events);
+ for (int i = 0; i < app_events.size(); ++i) {
+ DispatchAndDelete(app_events[i].release());
+ }
+}
+
+void ApplicationAndroid::SendKeyboardInject(SbKey key) {
+ write(keyboard_inject_writefd_, &key, sizeof(key));
+}
+
+extern "C" SB_EXPORT_PLATFORM void
+Java_dev_cobalt_coat_CobaltA11yHelper_nativeInjectKeyEvent(JNIEnv* env,
+ jobject unused_clazz,
+ jint key) {
+ ApplicationAndroid::Get()->SendKeyboardInject(static_cast<SbKey>(key));
+}
+
+#if SB_HAS(ON_SCREEN_KEYBOARD)
+
+void ApplicationAndroid::SbWindowShowOnScreenKeyboard(SbWindow window,
+ const char* input_text,
+ int ticket) {
+ JniEnvExt* env = JniEnvExt::Get();
+ jobject j_keyboard_editor = env->CallStarboardObjectMethodOrAbort(
+ "getKeyboardEditor", "()Ldev/cobalt/coat/KeyboardEditor;");
+ env->CallVoidMethodOrAbort(j_keyboard_editor, "showKeyboard", "()V");
+ // TODO: Fire kSbEventTypeWindowSizeChange and
+ // kSbEventTypeOnScreenKeyboardShown if necessary.
+ return;
+}
+
+void ApplicationAndroid::SbWindowHideOnScreenKeyboard(SbWindow window,
+ int ticket) {
+ JniEnvExt* env = JniEnvExt::Get();
+ jobject j_keyboard_editor = env->CallStarboardObjectMethodOrAbort(
+ "getKeyboardEditor", "()Ldev/cobalt/coat/KeyboardEditor;");
+ env->CallVoidMethodOrAbort(j_keyboard_editor, "hideKeyboard", "()V");
+ // TODO: Fire kSbEventTypeWindowSizeChange and
+ // kSbEventTypeOnScreenKeyboardHidden if necessary.
+ return;
+}
+
+extern "C" SB_EXPORT_PLATFORM void
+Java_dev_cobalt_coat_KeyboardInputConnection_nativeSendText(
+ JniEnvExt* env,
+ jobject unused_clazz,
+ jstring text) {
+ if (text) {
+ std::string utf_str = env->GetStringStandardUTFOrAbort(text);
+ ApplicationAndroid::Get()->SbWindowSendInputEvent(utf_str.c_str());
+ }
+}
+
+void DeleteSbInputDataWithText(void* ptr) {
+ SbInputData* data = static_cast<SbInputData*>(ptr);
+ const char* input_text = data->input_text;
+ data->input_text = NULL;
+ delete input_text;
+ ApplicationAndroid::DeleteDestructor<SbInputData>(ptr);
+}
+
+void ApplicationAndroid::SbWindowSendInputEvent(const char* input_text) {
+ char* text = SbStringDuplicate(input_text);
+ SbInputData* data = new SbInputData();
+ SbMemorySet(data, 0, sizeof(*data));
+ data->window = window_;
+ data->type = kSbInputEventTypeInput;
+ data->device_type = kSbInputDeviceTypeOnScreenKeyboard;
+ data->input_text = text;
+ Inject(new Event(kSbEventTypeInput, data, &DeleteSbInputDataWithText));
+ return;
+}
+
+#endif // SB_HAS(ON_SCREEN_KEYBOARD)
+
+bool ApplicationAndroid::OnSearchRequested() {
+ for (int i = 0; i < 2; i++) {
+ SbInputData* data = new SbInputData();
+ SbMemorySet(data, 0, sizeof(*data));
+ data->window = window_;
+ data->key = kSbKeyBrowserSearch;
+ data->type = (i == 0) ? kSbInputEventTypePress : kSbInputEventTypeUnpress;
+ Inject(new Event(kSbEventTypeInput, data, &DeleteDestructor<SbInputData>));
+ }
+ return true;
+}
+
+extern "C" SB_EXPORT_PLATFORM
+jboolean Java_dev_cobalt_coat_StarboardBridge_nativeOnSearchRequested(
+ JniEnvExt* env, jobject unused_this) {
+ return ApplicationAndroid::Get()->OnSearchRequested();
+}
+
+void ApplicationAndroid::HandleDeepLink(const char* link_url) {
+ if (link_url == NULL || link_url[0] == '\0') {
+ return;
+ }
+ char* deep_link = SbStringDuplicate(link_url);
+ SB_DCHECK(deep_link);
+ Inject(new Event(kSbEventTypeLink, deep_link, SbMemoryDeallocate));
+}
+
+extern "C" SB_EXPORT_PLATFORM
+void Java_dev_cobalt_coat_StarboardBridge_nativeHandleDeepLink(
+ JniEnvExt* env, jobject unused_this, jstring j_url) {
+ if (j_url) {
+ std::string utf_str = env->GetStringStandardUTFOrAbort(j_url);
+ ApplicationAndroid::Get()->HandleDeepLink(utf_str.c_str());
+ }
+}
+
+extern "C" SB_EXPORT_PLATFORM
+void Java_dev_cobalt_coat_StarboardBridge_nativeStopApp(
+ JniEnvExt* env, jobject unused_this, jint error_level) {
+ ApplicationAndroid::Get()->Stop(error_level);
+}
+
+} // namespace shared
+} // namespace android
+} // namespace starboard
diff --git a/src/starboard/android/shared/application_android.h b/src/starboard/android/shared/application_android.h
new file mode 100644
index 0000000..3631e03
--- /dev/null
+++ b/src/starboard/android/shared/application_android.h
@@ -0,0 +1,131 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_APPLICATION_ANDROID_H_
+#define STARBOARD_ANDROID_SHARED_APPLICATION_ANDROID_H_
+
+#include <android/looper.h>
+#include <android/native_window.h>
+
+#include "starboard/android/shared/input_events_generator.h"
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/common/scoped_ptr.h"
+#include "starboard/condition_variable.h"
+#include "starboard/configuration.h"
+#include "starboard/mutex.h"
+#include "starboard/shared/internal_only.h"
+#include "starboard/shared/starboard/application.h"
+#include "starboard/shared/starboard/queue_application.h"
+#include "starboard/types.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+// Android application receiving commands and input through ALooper.
+class ApplicationAndroid
+ : public ::starboard::shared::starboard::QueueApplication {
+ public:
+ struct AndroidCommand {
+ typedef enum {
+ kUndefined,
+ kStart,
+ kResume,
+ kPause,
+ kStop,
+ kInputQueueChanged,
+ kNativeWindowCreated,
+ kNativeWindowDestroyed,
+ kWindowFocusGained,
+ kWindowFocusLost,
+ } CommandType;
+
+ CommandType type;
+ void* data;
+ };
+
+ explicit ApplicationAndroid(ALooper* looper);
+ ~ApplicationAndroid() override;
+
+ static ApplicationAndroid* Get() {
+ return static_cast<ApplicationAndroid*>(
+ ::starboard::shared::starboard::Application::Get());
+ }
+
+ SbWindow CreateWindow(const SbWindowOptions* options);
+ bool DestroyWindow(SbWindow window);
+ bool OnSearchRequested();
+ void HandleDeepLink(const char* link_url);
+
+ void SendAndroidCommand(AndroidCommand::CommandType type, void* data);
+ void SendAndroidCommand(AndroidCommand::CommandType type) {
+ SendAndroidCommand(type, NULL);
+ }
+ void SendKeyboardInject(SbKey key);
+
+ void SbWindowShowOnScreenKeyboard(SbWindow window,
+ const char* input_text,
+ int ticket);
+ void SbWindowHideOnScreenKeyboard(SbWindow window, int ticket);
+ void SbWindowSendInputEvent(const char* input_text);
+
+ protected:
+ // --- Application overrides ---
+ void Initialize() override;
+ void Teardown() override;
+ bool IsStartImmediate() override { return false; }
+ void OnResume() override;
+
+ // --- QueueApplication overrides ---
+ bool MayHaveSystemEvents() override { return true; }
+ Event* WaitForSystemEventWithTimeout(SbTime time) override;
+ void WakeSystemEventWait() override;
+
+ private:
+ ALooper* looper_;
+ ANativeWindow* native_window_;
+ AInputQueue* input_queue_;
+
+ // Pipes attached to the looper.
+ int android_command_readfd_;
+ int android_command_writefd_;
+ int keyboard_inject_readfd_;
+ int keyboard_inject_writefd_;
+
+ // Synchronization for commands that change availability of Android resources
+ // such as the input_queue_ and/or native_window_.
+ Mutex android_command_mutex_;
+ ConditionVariable android_command_condition_;
+
+ // The last Activity lifecycle state command received.
+ AndroidCommand::CommandType activity_state_;
+
+ // The single open window, if any.
+ SbWindow window_;
+
+ scoped_ptr<InputEventsGenerator> input_events_generator_;
+
+ bool last_is_accessibility_high_contrast_text_enabled_;
+
+ // Methods to process pipes attached to the Looper.
+ void ProcessAndroidCommand();
+ void ProcessAndroidInput();
+ void ProcessKeyboardInject();
+};
+
+} // namespace shared
+} // namespace android
+} // namespace starboard
+
+#endif // STARBOARD_ANDROID_SHARED_APPLICATION_ANDROID_H_
diff --git a/src/starboard/android/shared/atomic_public.h b/src/starboard/android/shared/atomic_public.h
new file mode 100644
index 0000000..5bd90ca
--- /dev/null
+++ b/src/starboard/android/shared/atomic_public.h
@@ -0,0 +1,26 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_ATOMIC_PUBLIC_H_
+#define STARBOARD_ANDROID_SHARED_ATOMIC_PUBLIC_H_
+
+#include "starboard/atomic.h"
+
+#if SB_IS(COMPILER_GCC)
+#include "starboard/shared/gcc/atomic_gcc_public.h"
+#else
+#error "Unknown Android compiler."
+#endif
+
+#endif // STARBOARD_ANDROID_SHARED_ATOMIC_PUBLIC_H_
diff --git a/src/starboard/android/shared/audio_decoder.cc b/src/starboard/android/shared/audio_decoder.cc
new file mode 100644
index 0000000..5bd0905
--- /dev/null
+++ b/src/starboard/android/shared/audio_decoder.cc
@@ -0,0 +1,250 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/audio_decoder.h"
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/android/shared/media_common.h"
+#include "starboard/audio_sink.h"
+#include "starboard/log.h"
+#include "starboard/memory.h"
+
+// Can be locally set to |1| for verbose audio decoding. Verbose audio
+// decoding will log the following transitions that take place for each audio
+// unit:
+// T1: Our client passes an |InputBuffer| of audio data into us.
+// T2: We receive a corresponding media codec output buffer back from our
+// |MediaCodecBridge|.
+// T3: Our client reads a corresponding |DecodedAudio| out of us.
+//
+// Example usage for debugging audio playback:
+// $ adb logcat -c
+// $ adb logcat | tee log.txt
+// # Play video and get to frozen point.
+// $ CTRL-C
+// $ cat log.txt | grep -P 'T2: pts \d+' | wc -l
+// 523
+// $ cat log.txt | grep -P 'T3: pts \d+' | wc -l
+// 522
+// # Oh no, why isn't our client reading the audio we have ready to go?
+// # Time to go find out...
+#define STARBOARD_ANDROID_SHARED_AUDIO_DECODER_VERBOSE 0
+#if STARBOARD_ANDROID_SHARED_AUDIO_DECODER_VERBOSE
+#define VERBOSE_MEDIA_LOG() SB_LOG(INFO)
+#else
+#define VERBOSE_MEDIA_LOG() SB_EAT_STREAM_PARAMETERS
+#endif
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+namespace {
+
+SbMediaAudioSampleType GetSupportedSampleType() {
+ SB_DCHECK(SbAudioSinkIsAudioSampleTypeSupported(
+ kSbMediaAudioSampleTypeInt16Deprecated));
+ return kSbMediaAudioSampleTypeInt16Deprecated;
+}
+
+void* IncrementPointerByBytes(void* pointer, int offset) {
+ return static_cast<uint8_t*>(pointer) + offset;
+}
+
+} // namespace
+
+AudioDecoder::AudioDecoder(SbMediaAudioCodec audio_codec,
+ const SbMediaAudioHeader& audio_header,
+ SbDrmSystem drm_system)
+ : audio_codec_(audio_codec),
+ audio_header_(audio_header),
+ sample_type_(GetSupportedSampleType()),
+ output_sample_rate_(audio_header.samples_per_second),
+ output_channel_count_(audio_header.number_of_channels),
+ drm_system_(static_cast<DrmSystem*>(drm_system)) {
+ if (!InitializeCodec()) {
+ SB_LOG(ERROR) << "Failed to initialize audio decoder.";
+ }
+}
+
+AudioDecoder::~AudioDecoder() {}
+
+void AudioDecoder::Initialize(const OutputCB& output_cb,
+ const ErrorCB& error_cb) {
+ SB_DCHECK(BelongsToCurrentThread());
+ SB_DCHECK(output_cb);
+ SB_DCHECK(!output_cb_);
+ SB_DCHECK(error_cb);
+ SB_DCHECK(!error_cb_);
+ SB_DCHECK(media_decoder_);
+
+ output_cb_ = output_cb;
+ error_cb_ = error_cb;
+
+ media_decoder_->Initialize(error_cb_);
+}
+
+void AudioDecoder::Decode(const scoped_refptr<InputBuffer>& input_buffer,
+ const ConsumedCB& consumed_cb) {
+ SB_DCHECK(BelongsToCurrentThread());
+ SB_DCHECK(input_buffer);
+ SB_DCHECK(output_cb_);
+ SB_DCHECK(media_decoder_);
+
+ VERBOSE_MEDIA_LOG() << "T1: timestamp " << input_buffer->timestamp();
+
+ media_decoder_->WriteInputBuffer(input_buffer);
+
+ ScopedLock lock(decoded_audios_mutex_);
+ if (media_decoder_->GetNumberOfPendingTasks() + decoded_audios_.size() <=
+ kMaxPendingWorkSize) {
+ Schedule(consumed_cb);
+ } else {
+ consumed_cb_ = consumed_cb;
+ }
+}
+
+void AudioDecoder::WriteEndOfStream() {
+ SB_DCHECK(BelongsToCurrentThread());
+ SB_DCHECK(output_cb_);
+ SB_DCHECK(media_decoder_);
+
+ media_decoder_->WriteEndOfStream();
+}
+
+scoped_refptr<AudioDecoder::DecodedAudio> AudioDecoder::Read() {
+ SB_DCHECK(BelongsToCurrentThread());
+ SB_DCHECK(output_cb_);
+
+ scoped_refptr<DecodedAudio> result;
+ {
+ starboard::ScopedLock lock(decoded_audios_mutex_);
+ SB_DCHECK(!decoded_audios_.empty());
+ if (!decoded_audios_.empty()) {
+ result = decoded_audios_.front();
+ VERBOSE_MEDIA_LOG() << "T3: timestamp " << result->timestamp();
+ decoded_audios_.pop();
+ }
+ }
+
+ if (consumed_cb_) {
+ Schedule(consumed_cb_);
+ consumed_cb_ = nullptr;
+ }
+ return result;
+}
+
+void AudioDecoder::Reset() {
+ SB_DCHECK(BelongsToCurrentThread());
+ SB_DCHECK(output_cb_);
+
+ media_decoder_.reset();
+
+ if (!InitializeCodec()) {
+ // TODO: Communicate this failure to our clients somehow.
+ SB_LOG(ERROR) << "Failed to initialize codec after reset.";
+ }
+
+ consumed_cb_ = nullptr;
+
+ while (!decoded_audios_.empty()) {
+ decoded_audios_.pop();
+ }
+
+ CancelPendingJobs();
+}
+
+bool AudioDecoder::InitializeCodec() {
+ SB_DCHECK(!media_decoder_);
+ media_decoder_.reset(
+ new MediaDecoder(this, audio_codec_, audio_header_, drm_system_));
+ if (media_decoder_->is_valid()) {
+ if (error_cb_) {
+ media_decoder_->Initialize(error_cb_);
+ }
+ return true;
+ }
+ media_decoder_.reset();
+ return false;
+}
+
+void AudioDecoder::ProcessOutputBuffer(
+ MediaCodecBridge* media_codec_bridge,
+ const DequeueOutputResult& dequeue_output_result) {
+ SB_DCHECK(media_codec_bridge);
+ SB_DCHECK(output_cb_);
+ SB_DCHECK(dequeue_output_result.index >= 0);
+
+ if (dequeue_output_result.flags & BUFFER_FLAG_END_OF_STREAM) {
+ media_codec_bridge->ReleaseOutputBuffer(dequeue_output_result.index, false);
+ {
+ starboard::ScopedLock lock(decoded_audios_mutex_);
+ decoded_audios_.push(new DecodedAudio());
+ }
+
+ Schedule(output_cb_);
+ return;
+ }
+
+ ScopedJavaByteBuffer byte_buffer(
+ media_codec_bridge->GetOutputBuffer(dequeue_output_result.index));
+ SB_DCHECK(!byte_buffer.IsNull());
+
+ if (dequeue_output_result.num_bytes > 0) {
+ int16_t* data = static_cast<int16_t*>(IncrementPointerByBytes(
+ byte_buffer.address(), dequeue_output_result.offset));
+ int size = dequeue_output_result.num_bytes;
+ if (2 * audio_header_.samples_per_second == output_sample_rate_) {
+ // The audio is encoded using implicit HE-AAC. As the audio sink has
+ // been created already we try to down-mix the decoded data to half of
+ // its channels so the audio sink can play it with the correct pitch.
+ for (int i = 0; i < size / sizeof(int16_t); i++) {
+ data[i / 2] = (static_cast<int32_t>(data[i]) +
+ static_cast<int32_t>(data[i + 1]) / 2);
+ }
+ size /= 2;
+ }
+
+ scoped_refptr<DecodedAudio> decoded_audio = new DecodedAudio(
+ audio_header_.number_of_channels, GetSampleType(), GetStorageType(),
+ dequeue_output_result.presentation_time_microseconds, size);
+
+ SbMemoryCopy(decoded_audio->buffer(), data, size);
+ {
+ starboard::ScopedLock lock(decoded_audios_mutex_);
+ decoded_audios_.push(decoded_audio);
+ VERBOSE_MEDIA_LOG() << "T2: timestamp "
+ << decoded_audios_.front()->timestamp();
+ }
+ Schedule(output_cb_);
+ }
+
+ media_codec_bridge->ReleaseOutputBuffer(dequeue_output_result.index, false);
+}
+
+void AudioDecoder::RefreshOutputFormat(MediaCodecBridge* media_codec_bridge) {
+ AudioOutputFormatResult output_format =
+ media_codec_bridge->GetAudioOutputFormat();
+ if (output_format.status == MEDIA_CODEC_ERROR) {
+ SB_LOG(ERROR) << "|getOutputFormat| failed";
+ return;
+ }
+ output_sample_rate_ = output_format.sample_rate;
+ output_channel_count_ = output_format.channel_count;
+}
+
+} // namespace shared
+} // namespace android
+} // namespace starboard
diff --git a/src/starboard/android/shared/audio_decoder.h b/src/starboard/android/shared/audio_decoder.h
new file mode 100644
index 0000000..c18e4a4
--- /dev/null
+++ b/src/starboard/android/shared/audio_decoder.h
@@ -0,0 +1,100 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_AUDIO_DECODER_H_
+#define STARBOARD_ANDROID_SHARED_AUDIO_DECODER_H_
+
+#include <jni.h>
+
+#include <queue>
+
+#include "starboard/android/shared/drm_system.h"
+#include "starboard/android/shared/media_codec_bridge.h"
+#include "starboard/android/shared/media_decoder.h"
+#include "starboard/common/ref_counted.h"
+#include "starboard/media.h"
+#include "starboard/shared/internal_only.h"
+#include "starboard/shared/starboard/player/decoded_audio_internal.h"
+#include "starboard/shared/starboard/player/filter/audio_decoder_internal.h"
+#include "starboard/shared/starboard/player/job_queue.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+class AudioDecoder
+ : public ::starboard::shared::starboard::player::filter::AudioDecoder,
+ private ::starboard::shared::starboard::player::JobQueue::JobOwner,
+ private MediaDecoder::Host {
+ public:
+ AudioDecoder(SbMediaAudioCodec audio_codec,
+ const SbMediaAudioHeader& audio_header,
+ SbDrmSystem drm_system);
+ ~AudioDecoder() override;
+
+ void Initialize(const OutputCB& output_cb, const ErrorCB& error_cb) override;
+ void Decode(const scoped_refptr<InputBuffer>& input_buffer,
+ const ConsumedCB& consumed_cb) override;
+ void WriteEndOfStream() override;
+ scoped_refptr<DecodedAudio> Read() override;
+ void Reset() override;
+
+ SbMediaAudioSampleType GetSampleType() const override {
+ return sample_type_;
+ }
+ SbMediaAudioFrameStorageType GetStorageType() const override {
+ return kSbMediaAudioFrameStorageTypeInterleaved;
+ }
+ int GetSamplesPerSecond() const override {
+ return audio_header_.samples_per_second;
+ }
+
+ bool is_valid() const { return media_decoder_ != NULL; }
+
+ private:
+ // The maximum amount of work that can exist in the union of |EventQueue|,
+ // |pending_work| and |decoded_audios_|.
+ static const int kMaxPendingWorkSize = 64;
+
+ bool InitializeCodec();
+ void ProcessOutputBuffer(MediaCodecBridge* media_codec_bridge,
+ const DequeueOutputResult& output) override;
+ void RefreshOutputFormat(MediaCodecBridge* media_codec_bridge) override;
+ bool Tick(MediaCodecBridge* media_codec_bridge) override { return false; }
+ void OnFlushing() override {}
+
+ SbMediaAudioCodec audio_codec_;
+ SbMediaAudioHeader audio_header_;
+ SbMediaAudioSampleType sample_type_;
+
+ jint output_sample_rate_;
+ jint output_channel_count_;
+
+ DrmSystem* drm_system_;
+
+ OutputCB output_cb_;
+ ErrorCB error_cb_;
+ ConsumedCB consumed_cb_;
+
+ starboard::Mutex decoded_audios_mutex_;
+ std::queue<scoped_refptr<DecodedAudio> > decoded_audios_;
+
+ scoped_ptr<MediaDecoder> media_decoder_;
+};
+
+} // namespace shared
+} // namespace android
+} // namespace starboard
+
+#endif // STARBOARD_ANDROID_SHARED_AUDIO_DECODER_H_
diff --git a/src/starboard/android/shared/audio_sink_get_max_channels.cc b/src/starboard/android/shared/audio_sink_get_max_channels.cc
new file mode 100644
index 0000000..979f62f
--- /dev/null
+++ b/src/starboard/android/shared/audio_sink_get_max_channels.cc
@@ -0,0 +1,19 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/audio_sink.h"
+
+int SbAudioSinkGetMaxChannels() {
+ return 6;
+}
diff --git a/src/starboard/android/shared/audio_sink_get_nearest_supported_sample_frequency.cc b/src/starboard/android/shared/audio_sink_get_nearest_supported_sample_frequency.cc
new file mode 100644
index 0000000..bc753b9
--- /dev/null
+++ b/src/starboard/android/shared/audio_sink_get_nearest_supported_sample_frequency.cc
@@ -0,0 +1,27 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/audio_sink.h"
+
+#include "starboard/log.h"
+
+int SbAudioSinkGetNearestSupportedSampleFrequency(int sampling_frequency_hz) {
+ if (sampling_frequency_hz <= 0) {
+ SB_LOG(ERROR) << "Invalid audio sampling frequency "
+ << sampling_frequency_hz;
+ return 1;
+ }
+
+ return sampling_frequency_hz;
+}
diff --git a/src/starboard/android/shared/audio_sink_is_audio_frame_storage_type_supported.cc b/src/starboard/android/shared/audio_sink_is_audio_frame_storage_type_supported.cc
new file mode 100644
index 0000000..771410e
--- /dev/null
+++ b/src/starboard/android/shared/audio_sink_is_audio_frame_storage_type_supported.cc
@@ -0,0 +1,20 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/audio_sink.h"
+
+bool SbAudioSinkIsAudioFrameStorageTypeSupported(
+ SbMediaAudioFrameStorageType audio_frame_storage_type) {
+ return audio_frame_storage_type == kSbMediaAudioFrameStorageTypeInterleaved;
+}
diff --git a/src/starboard/android/shared/audio_sink_is_audio_sample_type_supported.cc b/src/starboard/android/shared/audio_sink_is_audio_sample_type_supported.cc
new file mode 100644
index 0000000..1de6614
--- /dev/null
+++ b/src/starboard/android/shared/audio_sink_is_audio_sample_type_supported.cc
@@ -0,0 +1,21 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/audio_sink.h"
+
+bool SbAudioSinkIsAudioSampleTypeSupported(
+ SbMediaAudioSampleType audio_sample_type) {
+ return audio_sample_type == kSbMediaAudioSampleTypeInt16Deprecated ||
+ audio_sample_type == kSbMediaAudioSampleTypeFloat32;
+}
diff --git a/src/starboard/android/shared/audio_track_audio_sink_type.cc b/src/starboard/android/shared/audio_track_audio_sink_type.cc
new file mode 100644
index 0000000..43cf47d
--- /dev/null
+++ b/src/starboard/android/shared/audio_track_audio_sink_type.cc
@@ -0,0 +1,414 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/audio_track_audio_sink_type.h"
+
+#include <algorithm>
+#include <deque>
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/common/scoped_ptr.h"
+#include "starboard/mutex.h"
+#include "starboard/thread.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+namespace {
+
+// The maximum number of frames that can be written to android audio track per
+// write request. If we don't set this cap for writing frames to audio track,
+// we will repeatedly allocate a large byte array which cannot be consumed by
+// audio track completely.
+const int kMaxFramesPerRequest = 65536;
+
+const jint kNoOffset = 0;
+
+// Helper function to compute the size of the two valid starboard audio sample
+// types.
+size_t GetSampleSize(SbMediaAudioSampleType sample_type) {
+ switch (sample_type) {
+ case kSbMediaAudioSampleTypeFloat32:
+ return sizeof(float);
+ case kSbMediaAudioSampleTypeInt16Deprecated:
+ return sizeof(int16_t);
+ }
+ SB_NOTREACHED();
+ return 0u;
+}
+
+int GetAudioFormatSampleType(SbMediaAudioSampleType sample_type) {
+ switch (sample_type) {
+ case kSbMediaAudioSampleTypeFloat32:
+ // Android AudioFormat.ENCODING_PCM_FLOAT.
+ return 4;
+ case kSbMediaAudioSampleTypeInt16Deprecated:
+ // Android AudioFormat.ENCODING_PCM_16BIT.
+ return 2;
+ }
+ SB_NOTREACHED();
+ return 0u;
+}
+
+void* IncrementPointerByBytes(void* pointer, size_t offset) {
+ return static_cast<uint8_t*>(pointer) + offset;
+}
+
+class AudioTrackAudioSink : public SbAudioSinkPrivate {
+ public:
+ AudioTrackAudioSink(
+ Type* type,
+ int channels,
+ int sampling_frequency_hz,
+ SbMediaAudioSampleType sample_type,
+ SbAudioSinkFrameBuffers frame_buffers,
+ int frames_per_channel,
+ SbAudioSinkUpdateSourceStatusFunc update_source_status_func,
+ SbAudioSinkConsumeFramesFunc consume_frame_func,
+ void* context);
+ ~AudioTrackAudioSink() override;
+
+ bool IsAudioTrackValid() const { return j_audio_track_bridge_; }
+ bool IsType(Type* type) override { return type_ == type; }
+ void SetPlaybackRate(double playback_rate) override {
+ SB_DCHECK(playback_rate >= 0.0);
+ if (playback_rate != 0.0 && playback_rate != 1.0) {
+ SB_NOTIMPLEMENTED() << "TODO: Only playback rates of 0.0 and 1.0 are "
+ "currently supported.";
+ playback_rate = (playback_rate > 0.0) ? 1.0 : 0.0;
+ }
+ ScopedLock lock(mutex_);
+ playback_rate_ = playback_rate;
+ }
+
+ void SetVolume(double volume) override;
+
+ private:
+ static void* ThreadEntryPoint(void* context);
+ void AudioThreadFunc();
+
+ Type* type_;
+ int channels_;
+ int sampling_frequency_hz_;
+ SbMediaAudioSampleType sample_type_;
+ void* frame_buffer_;
+ int frames_per_channel_;
+ SbAudioSinkUpdateSourceStatusFunc update_source_status_func_;
+ SbAudioSinkConsumeFramesFunc consume_frame_func_;
+ void* context_;
+ int last_playback_head_position_;
+ jobject j_audio_track_bridge_;
+ jobject j_audio_data_;
+
+ volatile bool quit_;
+ SbThread audio_out_thread_;
+
+ starboard::Mutex mutex_;
+ double playback_rate_;
+
+ int written_frames_;
+};
+
+AudioTrackAudioSink::AudioTrackAudioSink(
+ Type* type,
+ int channels,
+ int sampling_frequency_hz,
+ SbMediaAudioSampleType sample_type,
+ SbAudioSinkFrameBuffers frame_buffers,
+ int frames_per_channel,
+ SbAudioSinkUpdateSourceStatusFunc update_source_status_func,
+ SbAudioSinkConsumeFramesFunc consume_frame_func,
+ void* context)
+ : type_(type),
+ channels_(channels),
+ sampling_frequency_hz_(sampling_frequency_hz),
+ sample_type_(sample_type),
+ frame_buffer_(frame_buffers[0]),
+ frames_per_channel_(frames_per_channel),
+ update_source_status_func_(update_source_status_func),
+ consume_frame_func_(consume_frame_func),
+ context_(context),
+ last_playback_head_position_(0),
+ j_audio_track_bridge_(NULL),
+ j_audio_data_(NULL),
+ quit_(false),
+ audio_out_thread_(kSbThreadInvalid),
+ playback_rate_(1.0f),
+ written_frames_(0) {
+ SB_DCHECK(update_source_status_func_);
+ SB_DCHECK(consume_frame_func_);
+ SB_DCHECK(frame_buffer_);
+ SB_DCHECK(SbAudioSinkIsAudioSampleTypeSupported(sample_type));
+
+ JniEnvExt* env = JniEnvExt::Get();
+ ScopedLocalJavaRef<jobject> j_audio_output_manager(
+ env->CallStarboardObjectMethodOrAbort(
+ "getAudioOutputManager", "()Ldev/cobalt/media/AudioOutputManager;"));
+ jobject j_audio_track_bridge = env->CallObjectMethodOrAbort(
+ j_audio_output_manager.Get(), "createAudioTrackBridge",
+ "(IIII)Ldev/cobalt/media/AudioTrackBridge;",
+ GetAudioFormatSampleType(sample_type_), sampling_frequency_hz_, channels_,
+ frames_per_channel);
+ if (!j_audio_track_bridge) {
+ return;
+ }
+ j_audio_track_bridge_ = env->ConvertLocalRefToGlobalRef(j_audio_track_bridge);
+ if (sample_type_ == kSbMediaAudioSampleTypeFloat32) {
+ j_audio_data_ = env->NewFloatArray(channels_ * kMaxFramesPerRequest);
+ } else if (sample_type_ == kSbMediaAudioSampleTypeInt16Deprecated) {
+ j_audio_data_ = env->NewByteArray(channels_ * GetSampleSize(sample_type_) *
+ kMaxFramesPerRequest);
+ } else {
+ SB_NOTREACHED();
+ }
+ j_audio_data_ = env->ConvertLocalRefToGlobalRef(j_audio_data_);
+
+ audio_out_thread_ = SbThreadCreate(
+ 0, kSbThreadPriorityRealTime, kSbThreadNoAffinity, true,
+ "audio_track_audio_out", &AudioTrackAudioSink::ThreadEntryPoint, this);
+ SB_DCHECK(SbThreadIsValid(audio_out_thread_));
+}
+
+AudioTrackAudioSink::~AudioTrackAudioSink() {
+ quit_ = true;
+
+ if (SbThreadIsValid(audio_out_thread_)) {
+ SbThreadJoin(audio_out_thread_, NULL);
+ }
+
+ JniEnvExt* env = JniEnvExt::Get();
+ if (j_audio_track_bridge_) {
+ ScopedLocalJavaRef<jobject> j_audio_output_manager(
+ env->CallStarboardObjectMethodOrAbort(
+ "getAudioOutputManager",
+ "()Ldev/cobalt/media/AudioOutputManager;"));
+ env->CallVoidMethodOrAbort(
+ j_audio_output_manager.Get(), "destroyAudioTrackBridge",
+ "(Ldev/cobalt/media/AudioTrackBridge;)V", j_audio_track_bridge_);
+ env->DeleteGlobalRef(j_audio_track_bridge_);
+ j_audio_track_bridge_ = NULL;
+ }
+
+ if (j_audio_data_) {
+ env->DeleteGlobalRef(j_audio_data_);
+ j_audio_data_ = NULL;
+ }
+}
+
+// static
+void* AudioTrackAudioSink::ThreadEntryPoint(void* context) {
+ SB_DCHECK(context);
+ AudioTrackAudioSink* sink = reinterpret_cast<AudioTrackAudioSink*>(context);
+ sink->AudioThreadFunc();
+
+ return NULL;
+}
+
+void AudioTrackAudioSink::AudioThreadFunc() {
+ JniEnvExt* env = JniEnvExt::Get();
+ env->CallVoidMethodOrAbort(j_audio_track_bridge_, "play", "()V");
+
+ bool was_playing = true;
+
+ while (!quit_) {
+ ScopedLocalJavaRef<jobject> j_audio_timestamp(
+ env->CallObjectMethodOrAbort(j_audio_track_bridge_, "getAudioTimestamp",
+ "()Landroid/media/AudioTimestamp;"));
+
+ int playback_head_position =
+ env->GetLongFieldOrAbort(j_audio_timestamp.Get(), "framePosition", "J");
+ SbTime frames_consumed_at =
+ env->GetLongFieldOrAbort(j_audio_timestamp.Get(), "nanoTime", "J") /
+ 1000;
+
+ SB_DCHECK(playback_head_position >= last_playback_head_position_);
+
+ playback_head_position =
+ std::max(playback_head_position, last_playback_head_position_);
+ int frames_consumed = playback_head_position - last_playback_head_position_;
+ last_playback_head_position_ = playback_head_position;
+ frames_consumed = std::min(frames_consumed, written_frames_);
+
+ if (frames_consumed != 0) {
+ SB_DCHECK(frames_consumed >= 0);
+ consume_frame_func_(frames_consumed, frames_consumed_at, context_);
+ written_frames_ -= frames_consumed;
+ }
+
+ int frames_in_buffer;
+ int offset_in_frames;
+ bool is_playing;
+ bool is_eos_reached;
+ update_source_status_func_(&frames_in_buffer, &offset_in_frames,
+ &is_playing, &is_eos_reached, context_);
+
+ {
+ ScopedLock lock(mutex_);
+ if (playback_rate_ == 0.0) {
+ is_playing = false;
+ }
+ }
+
+ if (was_playing && !is_playing) {
+ was_playing = false;
+ env->CallVoidMethodOrAbort(j_audio_track_bridge_, "pause", "()V");
+ } else if (!was_playing && is_playing) {
+ was_playing = true;
+ env->CallVoidMethodOrAbort(j_audio_track_bridge_, "play", "()V");
+ }
+
+ if (!is_playing || frames_in_buffer == 0) {
+ SbThreadSleep(10 * kSbTimeMillisecond);
+ continue;
+ }
+
+ int start_position =
+ (offset_in_frames + written_frames_) % frames_per_channel_;
+ int expected_written_frames = 0;
+ if (frames_per_channel_ > offset_in_frames + written_frames_) {
+ expected_written_frames =
+ std::min(frames_per_channel_ - (offset_in_frames + written_frames_),
+ frames_in_buffer - written_frames_);
+ } else {
+ expected_written_frames = frames_in_buffer - written_frames_;
+ }
+
+ expected_written_frames =
+ std::min(expected_written_frames, kMaxFramesPerRequest);
+ if (expected_written_frames == 0) {
+ // It is possible that all the frames in buffer are written to the
+ // soundcard, but those are not being consumed.
+ continue;
+ }
+ SB_DCHECK(expected_written_frames > 0);
+ bool written_fully = false;
+
+ if (sample_type_ == kSbMediaAudioSampleTypeFloat32) {
+ int expected_written_size = expected_written_frames * channels_;
+ env->SetFloatArrayRegion(
+ static_cast<jfloatArray>(j_audio_data_), kNoOffset,
+ expected_written_size,
+ static_cast<const float*>(IncrementPointerByBytes(
+ frame_buffer_,
+ start_position * channels_ * GetSampleSize(sample_type_))));
+ int written =
+ env->CallIntMethodOrAbort(j_audio_track_bridge_, "write", "([FI)I",
+ j_audio_data_, expected_written_size);
+ SB_DCHECK(written >= 0);
+ SB_DCHECK(written % channels_ == 0);
+ written_frames_ += written / channels_;
+ written_fully = (written == expected_written_frames);
+ } else if (sample_type_ == kSbMediaAudioSampleTypeInt16Deprecated) {
+ int expected_written_size =
+ expected_written_frames * channels_ * GetSampleSize(sample_type_);
+ env->SetByteArrayRegion(
+ static_cast<jbyteArray>(j_audio_data_), kNoOffset,
+ expected_written_size,
+ static_cast<const jbyte*>(IncrementPointerByBytes(
+ frame_buffer_,
+ start_position * channels_ * GetSampleSize(sample_type_))));
+ int written =
+ env->CallIntMethodOrAbort(j_audio_track_bridge_, "write", "([BI)I",
+ j_audio_data_, expected_written_size);
+ SB_DCHECK(written >= 0);
+ SB_DCHECK(written % (channels_ * GetSampleSize(sample_type_)) == 0);
+ written_frames_ += written / (channels_ * GetSampleSize(sample_type_));
+ written_fully = (written == expected_written_frames);
+ } else {
+ SB_NOTREACHED();
+ }
+
+ auto unplayed_frames_in_time =
+ written_frames_ * kSbTimeSecond / sampling_frequency_hz_ -
+ (SbTimeGetMonotonicNow() - frames_consumed_at);
+ // As long as there is enough data in the buffer, run the loop in lower
+ // frequency to avoid taking too much CPU. Note that the threshold should
+ // be big enough to account for the unstable playback head reported at the
+ // beginning of the playback and during underrun.
+ if (playback_head_position > 0 &&
+ unplayed_frames_in_time > 500 * kSbTimeMillisecond) {
+ SbThreadSleep(40 * kSbTimeMillisecond);
+ } else if (!written_fully) {
+ // Only sleep if the buffer is nearly full and the last write is partial.
+ SbThreadSleep(1 * kSbTimeMillisecond);
+ }
+ }
+
+ // For an immediate stop, use pause(), followed by flush() to discard audio
+ // data that hasn't been played back yet.
+ env->CallVoidMethodOrAbort(j_audio_track_bridge_, "pause", "()V");
+ // Flushes the audio data currently queued for playback. Any data that has
+ // been written but not yet presented will be discarded.
+ env->CallVoidMethodOrAbort(j_audio_track_bridge_, "flush", "()V");
+}
+
+void AudioTrackAudioSink::SetVolume(double volume) {
+ auto* env = JniEnvExt::Get();
+ jint status = env->CallIntMethodOrAbort(j_audio_track_bridge_, "setVolume",
+ "(F)I", static_cast<float>(volume));
+ if (status != 0) {
+ SB_LOG(ERROR) << "Failed to set volume";
+ }
+}
+
+} // namespace
+
+SbAudioSink AudioTrackAudioSinkType::Create(
+ int channels,
+ int sampling_frequency_hz,
+ SbMediaAudioSampleType audio_sample_type,
+ SbMediaAudioFrameStorageType audio_frame_storage_type,
+ SbAudioSinkFrameBuffers frame_buffers,
+ int frames_per_channel,
+ SbAudioSinkUpdateSourceStatusFunc update_source_status_func,
+ SbAudioSinkConsumeFramesFunc consume_frames_func,
+ void* context) {
+ AudioTrackAudioSink* audio_sink = new AudioTrackAudioSink(
+ this, channels, sampling_frequency_hz, audio_sample_type, frame_buffers,
+ frames_per_channel, update_source_status_func, consume_frames_func,
+ context);
+ if (!audio_sink->IsAudioTrackValid()) {
+ SB_DLOG(ERROR)
+ << "AudioTrackAudioSinkType::Create failed to create audio track";
+ Destroy(audio_sink);
+ return kSbAudioSinkInvalid;
+ }
+ return audio_sink;
+}
+
+} // namespace shared
+} // namespace android
+} // namespace starboard
+
+namespace {
+SbAudioSinkPrivate::Type* audio_track_audio_sink_type_;
+} // namespace
+
+// static
+void SbAudioSinkPrivate::PlatformInitialize() {
+ SB_DCHECK(!audio_track_audio_sink_type_);
+ audio_track_audio_sink_type_ =
+ new starboard::android::shared::AudioTrackAudioSinkType;
+ SetPrimaryType(audio_track_audio_sink_type_);
+ EnableFallbackToStub();
+}
+
+// static
+void SbAudioSinkPrivate::PlatformTearDown() {
+ SB_DCHECK(audio_track_audio_sink_type_ == GetPrimaryType());
+ SetPrimaryType(NULL);
+ delete audio_track_audio_sink_type_;
+ audio_track_audio_sink_type_ = NULL;
+}
diff --git a/src/starboard/android/shared/audio_track_audio_sink_type.h b/src/starboard/android/shared/audio_track_audio_sink_type.h
new file mode 100644
index 0000000..6dda67e
--- /dev/null
+++ b/src/starboard/android/shared/audio_track_audio_sink_type.h
@@ -0,0 +1,58 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_AUDIO_TRACK_AUDIO_SINK_TYPE_H_
+#define STARBOARD_ANDROID_SHARED_AUDIO_TRACK_AUDIO_SINK_TYPE_H_
+
+#include "starboard/audio_sink.h"
+#include "starboard/configuration.h"
+#include "starboard/log.h"
+#include "starboard/shared/internal_only.h"
+#include "starboard/shared/starboard/audio_sink/audio_sink_internal.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+class AudioTrackAudioSinkType : public SbAudioSinkPrivate::Type {
+ public:
+ SbAudioSink Create(
+ int channels,
+ int sampling_frequency_hz,
+ SbMediaAudioSampleType audio_sample_type,
+ SbMediaAudioFrameStorageType audio_frame_storage_type,
+ SbAudioSinkFrameBuffers frame_buffers,
+ int frames_per_channel,
+ SbAudioSinkUpdateSourceStatusFunc update_source_status_func,
+ SbAudioSinkConsumeFramesFunc consume_frames_func,
+ void* context);
+
+ bool IsValid(SbAudioSink audio_sink) override {
+ return audio_sink != kSbAudioSinkInvalid && audio_sink->IsType(this);
+ }
+
+ void Destroy(SbAudioSink audio_sink) override {
+ if (audio_sink != kSbAudioSinkInvalid && !IsValid(audio_sink)) {
+ SB_LOG(WARNING) << "audio_sink is invalid.";
+ return;
+ }
+ delete audio_sink;
+ }
+};
+
+} // namespace shared
+} // namespace android
+} // namespace starboard
+
+#endif // STARBOARD_ANDROID_SHARED_AUDIO_TRACK_AUDIO_SINK_TYPE_H_
diff --git a/src/starboard/android/shared/cobalt/android.cc b/src/starboard/android/shared/cobalt/android.cc
new file mode 100644
index 0000000..ab1baa1
--- /dev/null
+++ b/src/starboard/android/shared/cobalt/android.cc
@@ -0,0 +1,33 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/cobalt/android.h"
+
+namespace cobalt {
+namespace webapi_extension {
+
+Android::Android(const scoped_refptr<::cobalt::dom::Window>& window) {
+ UNREFERENCED_PARAMETER(window);
+
+ feedback_service_ = new FeedbackService();
+}
+
+void Android::TraceMembers(script::Tracer* tracer) {
+ tracer->Trace(feedback_service_);
+}
+
+Android::~Android() {}
+
+} // namespace webapi_extension
+} // namespace cobalt
diff --git a/src/starboard/android/shared/cobalt/android.h b/src/starboard/android/shared/cobalt/android.h
new file mode 100644
index 0000000..b21d533
--- /dev/null
+++ b/src/starboard/android/shared/cobalt/android.h
@@ -0,0 +1,47 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_COBALT_ANDROID_H_
+#define STARBOARD_ANDROID_SHARED_COBALT_ANDROID_H_
+
+#include "cobalt/dom/window.h"
+#include "cobalt/script/wrappable.h"
+#include "starboard/android/shared/cobalt/feedback_service.h"
+
+namespace cobalt {
+namespace webapi_extension {
+
+class Android : public ::cobalt::script::Wrappable {
+ public:
+ explicit Android(const scoped_refptr<::cobalt::dom::Window>& window);
+
+ const scoped_refptr<FeedbackService>& feedback_service() const {
+ return feedback_service_;
+ }
+
+ DEFINE_WRAPPABLE_TYPE(Android);
+ void TraceMembers(script::Tracer* tracer) override;
+
+ private:
+ ~Android() override;
+
+ scoped_refptr<FeedbackService> feedback_service_;
+
+ DISALLOW_COPY_AND_ASSIGN(Android);
+};
+
+} // namespace webapi_extension
+} // namespace cobalt
+
+#endif // STARBOARD_ANDROID_SHARED_COBALT_ANDROID_H_
diff --git a/src/starboard/android/shared/cobalt/android.idl b/src/starboard/android/shared/cobalt/android.idl
new file mode 100644
index 0000000..18e71e7
--- /dev/null
+++ b/src/starboard/android/shared/cobalt/android.idl
@@ -0,0 +1,18 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// The Cobalt Web Extension for Android.
+interface Android {
+ readonly attribute FeedbackService feedbackService;
+};
diff --git a/src/starboard/android/shared/cobalt/android_media_session_client.cc b/src/starboard/android/shared/cobalt/android_media_session_client.cc
new file mode 100644
index 0000000..25b9323
--- /dev/null
+++ b/src/starboard/android/shared/cobalt/android_media_session_client.cc
@@ -0,0 +1,355 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/cobalt/android_media_session_client.h"
+
+#include "base/memory/scoped_ptr.h"
+#include "base/time.h"
+#include "cobalt/media_session/media_session_action_details.h"
+#include "cobalt/media_session/media_session_client.h"
+#include "cobalt/script/sequence.h"
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/log.h"
+#include "starboard/mutex.h"
+#include "starboard/once.h"
+#include "starboard/player.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+namespace cobalt {
+
+using ::cobalt::media_session::MediaImage;
+using ::cobalt::media_session::MediaMetadata;
+using ::cobalt::media_session::MediaSession;
+using ::cobalt::media_session::MediaSessionAction;
+using ::cobalt::media_session::MediaSessionActionDetails;
+using ::cobalt::media_session::MediaSessionClient;
+using ::cobalt::media_session::MediaSessionPlaybackState;
+using ::cobalt::media_session::kMediaSessionActionPause;
+using ::cobalt::media_session::kMediaSessionActionPlay;
+using ::cobalt::media_session::kMediaSessionActionSeek;
+using ::cobalt::media_session::kMediaSessionActionSeekbackward;
+using ::cobalt::media_session::kMediaSessionActionSeekforward;
+using ::cobalt::media_session::kMediaSessionActionPrevioustrack;
+using ::cobalt::media_session::kMediaSessionActionNexttrack;
+using ::cobalt::media_session::kMediaSessionPlaybackStateNone;
+using ::cobalt::media_session::kMediaSessionPlaybackStatePaused;
+using ::cobalt::media_session::kMediaSessionPlaybackStatePlaying;
+
+using MediaImageSequence = ::cobalt::script::Sequence<MediaImage>;
+
+using ::starboard::android::shared::JniEnvExt;
+using ::starboard::android::shared::ScopedLocalJavaRef;
+
+namespace {
+
+// These constants are from android.media.session.PlaybackState
+const jlong kPlaybackStateActionStop = 1 << 0; // not supported
+const jlong kPlaybackStateActionPause = 1 << 1;
+const jlong kPlaybackStateActionPlay = 1 << 2;
+const jlong kPlaybackStateActionRewind = 1 << 3;
+const jlong kPlaybackStateActionSkipToPrevious = 1 << 4;
+const jlong kPlaybackStateActionSkipToNext = 1 << 5;
+const jlong kPlaybackStateActionFastForward = 1 << 6;
+const jlong kPlaybackStateActionSetRating = 1 << 7; // not supported
+const jlong kPlaybackStateActionSeekTo = 1 << 8;
+
+// Converts a MediaSessionClient::AvailableActions bitset into
+// a android.media.session.PlaybackState jlong bitset.
+jlong MediaSessionActionsToPlaybackStateActions(
+ const MediaSessionClient::AvailableActionsSet& actions) {
+ jlong result = 0;
+ if (actions[kMediaSessionActionPause]) {
+ result |= kPlaybackStateActionPause;
+ }
+ if (actions[kMediaSessionActionPlay]) {
+ result |= kPlaybackStateActionPlay;
+ }
+ if (actions[kMediaSessionActionSeekbackward]) {
+ result |= kPlaybackStateActionRewind;
+ }
+ if (actions[kMediaSessionActionPrevioustrack]) {
+ result |= kPlaybackStateActionSkipToPrevious;
+ }
+ if (actions[kMediaSessionActionNexttrack]) {
+ result |= kPlaybackStateActionSkipToNext;
+ }
+ if (actions[kMediaSessionActionSeekforward]) {
+ result |= kPlaybackStateActionFastForward;
+ }
+ if (actions[kMediaSessionActionSeek]) {
+ result |= kPlaybackStateActionSeekTo;
+ }
+ return result;
+}
+
+PlaybackState MediaSessionPlaybackStateToPlaybackState(
+ MediaSessionPlaybackState in_state) {
+ switch (in_state) {
+ case kMediaSessionPlaybackStatePlaying:
+ return kPlaying;
+ case kMediaSessionPlaybackStatePaused:
+ return kPaused;
+ case kMediaSessionPlaybackStateNone:
+ return kNone;
+ }
+}
+
+MediaSessionAction PlaybackStateActionToMediaSessionAction(jlong action) {
+ MediaSessionAction result;
+ switch (action) {
+ case kPlaybackStateActionPause:
+ result = kMediaSessionActionPause;
+ break;
+ case kPlaybackStateActionPlay:
+ result = kMediaSessionActionPlay;
+ break;
+ case kPlaybackStateActionRewind:
+ result = kMediaSessionActionSeekbackward;
+ break;
+ case kPlaybackStateActionSkipToPrevious:
+ result = kMediaSessionActionPrevioustrack;
+ break;
+ case kPlaybackStateActionSkipToNext:
+ result = kMediaSessionActionNexttrack;
+ break;
+ case kPlaybackStateActionFastForward:
+ result = kMediaSessionActionSeekforward;
+ break;
+ case kPlaybackStateActionSeekTo:
+ result = kMediaSessionActionSeek;
+ break;
+ default:
+ SB_NOTREACHED() << "Unsupported MediaSessionAction 0x"
+ << std::hex << action;
+ result = static_cast<MediaSessionAction>(-1);
+ }
+ return result;
+}
+
+MediaSessionPlaybackState PlaybackStateToMediaSessionPlaybackState(
+ PlaybackState state) {
+ MediaSessionPlaybackState result;
+ switch (state) {
+ case kPlaying:
+ result = kMediaSessionPlaybackStatePlaying;
+ break;
+ case kPaused:
+ result = kMediaSessionPlaybackStatePaused;
+ break;
+ case kNone:
+ result = kMediaSessionPlaybackStateNone;
+ break;
+ default:
+ SB_NOTREACHED() << "Unsupported PlaybackState " << state;
+ result = static_cast<MediaSessionPlaybackState>(-1);
+ }
+ return result;
+}
+
+} // namespace
+
+class AndroidMediaSessionClient : public MediaSessionClient {
+ static SbOnceControl once_flag;
+ static SbMutex mutex;
+ // The last MediaSessionClient to become active, or null.
+ // Used to route Java callbacks.
+ // In practice, only one MediaSessionClient will become active at a time.
+ // Protected by "mutex"
+ static AndroidMediaSessionClient* active_client;
+
+ // TODO: Pass the necessary info through web MediaSession so we don't need to
+ // short-circuit to the player implementation to get info about the playback.
+ static SbPlayer active_player;
+
+ static void OnceInit() { SbMutexCreate(&mutex); }
+
+ public:
+ static void NativeInvokeAction(jlong action, jlong seek_ms) {
+ SbOnce(&once_flag, OnceInit);
+ SbMutexAcquire(&mutex);
+
+ if (active_client != NULL) {
+ MediaSessionAction cobalt_action =
+ PlaybackStateActionToMediaSessionAction(action);
+ active_client->InvokeAction(scoped_ptr<MediaSessionActionDetails::Data>(
+ new MediaSessionActionDetails::Data(cobalt_action,
+ seek_ms / 1000.0)));
+ }
+
+ SbMutexRelease(&mutex);
+ }
+
+ static void UpdateActiveSessionPlatformPlaybackState(
+ MediaSessionPlaybackState state) {
+ SbOnce(&once_flag, OnceInit);
+ SbMutexAcquire(&mutex);
+
+ if (active_client != NULL) {
+ active_client->UpdatePlatformPlaybackState(state);
+ }
+
+ SbMutexRelease(&mutex);
+ }
+
+ static void UpdateActiveSessionPlatformPlayer(SbPlayer player) {
+ SbOnce(&once_flag, OnceInit);
+ SbMutexAcquire(&mutex);
+
+ active_player = player;
+
+ SbMutexRelease(&mutex);
+ }
+
+ AndroidMediaSessionClient() {}
+
+ virtual ~AndroidMediaSessionClient() {
+ SbOnce(&once_flag, OnceInit);
+ SbMutexAcquire(&mutex);
+ if (active_client == this) {
+ active_client = NULL;
+ }
+ SbMutexRelease(&mutex);
+ }
+
+ void OnMediaSessionChanged() override {
+ JniEnvExt* env = JniEnvExt::Get();
+
+ jint playback_state =
+ MediaSessionPlaybackStateToPlaybackState(GetActualPlaybackState());
+
+ SbOnce(&once_flag, OnceInit);
+ SbMutexAcquire(&mutex);
+ if (playback_state != kNone) {
+ active_client = this;
+ } else if (active_client == this) {
+ active_client = NULL;
+ }
+
+ SbPlayerInfo2 player_info;
+ SbMemorySet(&player_info, 0, sizeof(player_info));
+ if (active_player != kSbPlayerInvalid) {
+ SbPlayerGetInfo2(active_player, &player_info);
+ }
+ SbMutexRelease(&mutex);
+
+ jlong playback_state_actions =
+ MediaSessionActionsToPlaybackStateActions(GetAvailableActions());
+
+ scoped_refptr<MediaSession> media_session(GetMediaSession());
+ scoped_refptr<MediaMetadata> media_metadata(media_session->metadata());
+
+ ScopedLocalJavaRef<jstring> j_title;
+ ScopedLocalJavaRef<jstring> j_artist;
+ ScopedLocalJavaRef<jstring> j_album;
+ ScopedLocalJavaRef<jobjectArray> j_artwork;
+
+ if (media_metadata) {
+ j_title.Reset(
+ env->NewStringStandardUTFOrAbort(media_metadata->title().c_str()));
+ j_artist.Reset(
+ env->NewStringStandardUTFOrAbort(media_metadata->artist().c_str()));
+ j_album.Reset(
+ env->NewStringStandardUTFOrAbort(media_metadata->album().c_str()));
+
+ const MediaImageSequence& artwork(media_metadata->artwork());
+ if (!artwork.empty()) {
+ ScopedLocalJavaRef<jclass> media_image_class(
+ env->FindClassExtOrAbort("dev/cobalt/media/MediaImage"));
+ jmethodID media_image_constructor = env->GetMethodID(
+ media_image_class.Get(), "<init>",
+ "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V");
+ env->AbortOnException();
+
+ j_artwork.Reset(static_cast<jobjectArray>(env->NewObjectArray(
+ artwork.size(), media_image_class.Get(), NULL)));
+ env->AbortOnException();
+
+ ScopedLocalJavaRef<jstring> j_src;
+ ScopedLocalJavaRef<jstring> j_sizes;
+ ScopedLocalJavaRef<jstring> j_type;
+ for (MediaImageSequence::size_type i = 0; i < artwork.size(); i++) {
+ const MediaImage& media_image(artwork.at(i));
+ j_src.Reset(
+ env->NewStringStandardUTFOrAbort(media_image.src().c_str()));
+ j_sizes.Reset(
+ env->NewStringStandardUTFOrAbort(media_image.sizes().c_str()));
+ j_type.Reset(
+ env->NewStringStandardUTFOrAbort(media_image.type().c_str()));
+
+ ScopedLocalJavaRef<jobject> j_media_image(
+ env->NewObject(media_image_class.Get(), media_image_constructor,
+ j_src.Get(), j_sizes.Get(), j_type.Get()));
+
+ env->SetObjectArrayElement(j_artwork.Get(), i, j_media_image.Get());
+ }
+ }
+ }
+
+ env->CallStarboardVoidMethodOrAbort(
+ "updateMediaSession",
+ "(IJJFLjava/lang/String;Ljava/lang/String;Ljava/lang/String;"
+ "[Ldev/cobalt/media/MediaImage;)V",
+ playback_state, playback_state_actions,
+ player_info.current_media_timestamp / kSbTimeMillisecond,
+ static_cast<jfloat>(player_info.playback_rate),
+ j_title.Get(), j_artist.Get(), j_album.Get(), j_artwork.Get());
+ }
+};
+
+SbOnceControl AndroidMediaSessionClient::once_flag = SB_ONCE_INITIALIZER;
+SbMutex AndroidMediaSessionClient::mutex;
+AndroidMediaSessionClient* AndroidMediaSessionClient::active_client = NULL;
+SbPlayer AndroidMediaSessionClient::active_player = kSbPlayerInvalid;
+
+void UpdateActiveSessionPlatformPlaybackState(PlaybackState state) {
+ MediaSessionPlaybackState media_session_state =
+ PlaybackStateToMediaSessionPlaybackState(state);
+
+ AndroidMediaSessionClient::UpdateActiveSessionPlatformPlaybackState(
+ media_session_state);
+}
+
+void UpdateActiveSessionPlatformPlayer(SbPlayer player) {
+ AndroidMediaSessionClient::UpdateActiveSessionPlatformPlayer(player);
+}
+
+} // namespace cobalt
+} // namespace shared
+} // namespace android
+} // namespace starboard
+
+using starboard::android::shared::cobalt::AndroidMediaSessionClient;
+
+extern "C" SB_EXPORT_PLATFORM
+void Java_dev_cobalt_media_CobaltMediaSession_nativeInvokeAction(
+ JNIEnv* env,
+ jclass unused_clazz,
+ jlong action,
+ jlong seek_ms) {
+ AndroidMediaSessionClient::NativeInvokeAction(action, seek_ms);
+}
+
+namespace cobalt {
+namespace media_session {
+
+// static
+scoped_ptr<MediaSessionClient> MediaSessionClient::Create() {
+ return make_scoped_ptr<MediaSessionClient>(new AndroidMediaSessionClient());
+}
+
+} // namespace media_session
+} // namespace cobalt
diff --git a/src/starboard/android/shared/cobalt/android_media_session_client.h b/src/starboard/android/shared/cobalt/android_media_session_client.h
new file mode 100644
index 0000000..284e165
--- /dev/null
+++ b/src/starboard/android/shared/cobalt/android_media_session_client.h
@@ -0,0 +1,39 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_COBALT_ANDROID_MEDIA_SESSION_CLIENT_H_
+#define STARBOARD_ANDROID_SHARED_COBALT_ANDROID_MEDIA_SESSION_CLIENT_H_
+
+#include "starboard/player.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+namespace cobalt {
+
+// Duplicated in CobaltMediaSession.java
+enum PlaybackState { kPlaying = 0, kPaused = 1, kNone = 2 };
+
+void UpdateActiveSessionPlatformPlaybackState(PlaybackState state);
+
+// TODO: Pass the necessary info through web MediaSession so we don't need to
+// short-circuit to the player implementation to get info about the playback.
+void UpdateActiveSessionPlatformPlayer(SbPlayer player);
+
+} // namespace cobalt
+} // namespace shared
+} // namespace android
+} // namespace starboard
+
+#endif // STARBOARD_ANDROID_SHARED_COBALT_ANDROID_MEDIA_SESSION_CLIENT_H_
diff --git a/src/starboard/android/shared/cobalt/android_user_authorizer.cc b/src/starboard/android/shared/cobalt/android_user_authorizer.cc
new file mode 100644
index 0000000..a84b653
--- /dev/null
+++ b/src/starboard/android/shared/cobalt/android_user_authorizer.cc
@@ -0,0 +1,127 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/cobalt/android_user_authorizer.h"
+
+#include "base/time.h"
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/log.h"
+
+// We're in starboard source, but compiled with Cobalt.
+#define STARBOARD_IMPLEMENTATION
+#include "starboard/shared/nouser/user_internal.h"
+#undef STARBOARD_IMPLEMENTATION
+
+namespace starboard {
+namespace android {
+namespace shared {
+namespace cobalt {
+
+AndroidUserAuthorizer::AndroidUserAuthorizer() : shutdown_(false) {
+ JniEnvExt* env = JniEnvExt::Get();
+ jobject local_ref = env->CallStarboardObjectMethodOrAbort(
+ "getUserAuthorizer", "()Ldev/cobalt/account/UserAuthorizer;");
+ j_user_authorizer_ = env->ConvertLocalRefToGlobalRef(local_ref);
+}
+
+AndroidUserAuthorizer::~AndroidUserAuthorizer() {
+ JniEnvExt* env = JniEnvExt::Get();
+ env->DeleteGlobalRef(j_user_authorizer_);
+}
+
+void AndroidUserAuthorizer::Shutdown() {
+ shutdown_ = true;
+ JniEnvExt* env = JniEnvExt::Get();
+ env->CallVoidMethodOrAbort(j_user_authorizer_, "interrupt", "()V");
+}
+
+scoped_ptr<AccessToken> AndroidUserAuthorizer::AuthorizeUser(SbUser user) {
+ SB_DCHECK(user == &::starboard::shared::nouser::g_user);
+ if (shutdown_) {
+ DLOG(WARNING) << "No-op AuthorizeUser after shutdown";
+ return scoped_ptr<AccessToken>(NULL);
+ }
+ JniEnvExt* env = JniEnvExt::Get();
+ ScopedLocalJavaRef<jobject> j_token(
+ env->CallObjectMethodOrAbort(j_user_authorizer_, "authorizeUser",
+ "()Ldev/cobalt/account/AccessToken;"));
+ return CreateAccessToken(j_token.Get());
+}
+
+bool AndroidUserAuthorizer::DeauthorizeUser(SbUser user) {
+ SB_DCHECK(user == &::starboard::shared::nouser::g_user);
+ if (shutdown_) {
+ DLOG(WARNING) << "No-op DeauthorizeUser after shutdown";
+ return false;
+ }
+ JniEnvExt* env = JniEnvExt::Get();
+ return env->CallBooleanMethodOrAbort(j_user_authorizer_, "deauthorizeUser",
+ "()Z");
+}
+
+scoped_ptr<AccessToken>
+AndroidUserAuthorizer::RefreshAuthorization(SbUser user) {
+ SB_DCHECK(user == &::starboard::shared::nouser::g_user);
+ if (shutdown_) {
+ DLOG(WARNING) << "No-op RefreshAuthorization after shutdown";
+ return scoped_ptr<AccessToken>(NULL);
+ }
+ JniEnvExt* env = JniEnvExt::Get();
+ ScopedLocalJavaRef<jobject> j_token(
+ env->CallObjectMethodOrAbort(j_user_authorizer_, "refreshAuthorization",
+ "()Ldev/cobalt/account/AccessToken;"));
+ return CreateAccessToken(j_token.Get());
+}
+
+scoped_ptr<AccessToken>
+AndroidUserAuthorizer::CreateAccessToken(jobject j_token) {
+ if (!j_token) {
+ return scoped_ptr<AccessToken>(NULL);
+ }
+
+ JniEnvExt* env = JniEnvExt::Get();
+ scoped_ptr<AccessToken> access_token(new AccessToken());
+
+ ScopedLocalJavaRef<jstring> j_token_string(env->CallObjectMethodOrAbort(
+ j_token, "getTokenValue", "()Ljava/lang/String;"));
+ if (j_token_string) {
+ access_token->token_value =
+ env->GetStringStandardUTFOrAbort(j_token_string.Get());
+ }
+
+ jlong j_expiry = env->CallLongMethodOrAbort(
+ j_token, "getExpirySeconds", "()J");
+ if (j_expiry > 0) {
+ access_token->expiry =
+ base::Time::UnixEpoch() + base::TimeDelta::FromSeconds(j_expiry);
+ }
+
+ return access_token.Pass();
+}
+
+} // namespace cobalt
+} // namespace shared
+} // namespace android
+} // namespace starboard
+
+namespace cobalt {
+namespace account {
+
+UserAuthorizer* UserAuthorizer::Create() {
+ return new ::starboard::android::shared::cobalt::AndroidUserAuthorizer();
+}
+
+} // namespace account
+} // namespace cobalt
diff --git a/src/starboard/android/shared/cobalt/android_user_authorizer.h b/src/starboard/android/shared/cobalt/android_user_authorizer.h
new file mode 100644
index 0000000..6e9cadc
--- /dev/null
+++ b/src/starboard/android/shared/cobalt/android_user_authorizer.h
@@ -0,0 +1,63 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_COBALT_ANDROID_USER_AUTHORIZER_H_
+#define STARBOARD_ANDROID_SHARED_COBALT_ANDROID_USER_AUTHORIZER_H_
+
+#include "cobalt/account/user_authorizer.h"
+
+#include "starboard/android/shared/jni_env_ext.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+namespace cobalt {
+
+using ::cobalt::account::AccessToken;
+
+// Android implementation of UserAuthorizer, using the single 'nouser' SbUser to
+// represent the Android platform running our app. Unlike game consoles which
+// launch the app as a particular platform user, Android always launches the app
+// as the same platform "user" no matter what accounts may be on the device.
+//
+// Signing-in is a higher-level concept that is implemented by the Android app
+// using the Android AccountManager and/or Google Play Services to select an
+// account and get auth tokens for the selected account to make "signed-in"
+// requests. Account selection is accomplished by prompting the user as needed
+// when getting a token with AuthorizeUser(), and remembering the selected
+// account to be used without prompting in RefreshAuthorization().
+class AndroidUserAuthorizer : public ::cobalt::account::UserAuthorizer {
+ public:
+ AndroidUserAuthorizer();
+ ~AndroidUserAuthorizer() override;
+
+ scoped_ptr<AccessToken> AuthorizeUser(SbUser user) override;
+ bool DeauthorizeUser(SbUser user) override;
+ scoped_ptr<AccessToken> RefreshAuthorization(SbUser user) override;
+ void Shutdown() override;
+
+ private:
+ scoped_ptr<AccessToken> CreateAccessToken(jobject j_token);
+
+ jobject j_user_authorizer_;
+
+ bool shutdown_;
+};
+
+} // namespace cobalt
+} // namespace shared
+} // namespace android
+} // namespace starboard
+
+#endif // STARBOARD_ANDROID_SHARED_COBALT_ANDROID_USER_AUTHORIZER_H_
diff --git a/src/starboard/android/shared/cobalt/android_webapi_extension.cc b/src/starboard/android/shared/cobalt/android_webapi_extension.cc
new file mode 100644
index 0000000..827c702
--- /dev/null
+++ b/src/starboard/android/shared/cobalt/android_webapi_extension.cc
@@ -0,0 +1,38 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/browser/webapi_extension.h"
+
+#include "base/compiler_specific.h"
+#include "cobalt/script/global_environment.h"
+#include "starboard/android/shared/cobalt/android.h"
+
+namespace cobalt {
+namespace browser {
+
+base::optional<std::string> GetWebAPIExtensionObjectPropertyName() {
+ return std::string("android");
+}
+
+scoped_refptr<::cobalt::script::Wrappable> CreateWebAPIExtensionObject(
+ const scoped_refptr<::cobalt::dom::Window>& window,
+ ::cobalt::script::GlobalEnvironment* global_environment) {
+ UNREFERENCED_PARAMETER(global_environment);
+
+ return scoped_refptr<::cobalt::script::Wrappable>(
+ new webapi_extension::Android(window));
+}
+
+} // namespace browser
+} // namespace cobalt
diff --git a/src/starboard/android/shared/cobalt/android_webapi_extension.gyp b/src/starboard/android/shared/cobalt/android_webapi_extension.gyp
new file mode 100644
index 0000000..0c9511e
--- /dev/null
+++ b/src/starboard/android/shared/cobalt/android_webapi_extension.gyp
@@ -0,0 +1,33 @@
+# Copyright 2018 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+{
+ 'targets': [
+ {
+ 'target_name': 'android_webapi_extension',
+ 'type': 'static_library',
+ 'sources': [
+ 'android.h',
+ 'android.cc',
+ 'android_webapi_extension.cc',
+ 'feedback_service.h',
+ 'feedback_service.cc',
+ ],
+ 'dependencies': [
+ '<(DEPTH)/cobalt/dom/dom.gyp:dom',
+ '<(DEPTH)/cobalt/script/script.gyp:script',
+ '<(DEPTH)/base/base.gyp:base',
+ ],
+ },
+ ],
+}
diff --git a/src/starboard/android/shared/cobalt/cobalt_platform.gyp b/src/starboard/android/shared/cobalt/cobalt_platform.gyp
new file mode 100644
index 0000000..6f6379a
--- /dev/null
+++ b/src/starboard/android/shared/cobalt/cobalt_platform.gyp
@@ -0,0 +1,30 @@
+# Copyright 2017 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+{
+ 'targets': [
+ {
+ 'target_name': 'cobalt_platform',
+ 'type': 'static_library',
+ 'sources': [
+ 'android_user_authorizer.h',
+ 'android_user_authorizer.cc',
+ 'android_media_session_client.cc',
+ ],
+ 'dependencies': [
+ '<(DEPTH)/cobalt/media_session/media_session.gyp:media_session'
+ ],
+ },
+ ],
+}
diff --git a/src/starboard/android/shared/cobalt/configuration.gypi b/src/starboard/android/shared/cobalt/configuration.gypi
new file mode 100644
index 0000000..35647ce
--- /dev/null
+++ b/src/starboard/android/shared/cobalt/configuration.gypi
@@ -0,0 +1,62 @@
+# Copyright 2017 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Cobalt-on-Android-specific configuration for Starboard.
+
+{
+ 'variables': {
+ 'in_app_dial': 0,
+
+ 'custom_media_session_client': 1,
+ 'enable_account_manager': 1,
+ 'enable_map_to_mesh': 1,
+
+ # Some Android devices do not advertise their support for
+ # EGL_SWAP_BEHAVIOR_PRESERVED_BIT properly, so, we play it safe and disable
+ # relying on it for Android.
+ 'render_dirty_region_only': 0,
+
+ # The 'android_system' font package installs only minimal fonts, with a
+ # fonts.xml referencing the superset of font files we expect to find on any
+ # Android platform. The Android SbFileOpen implementation falls back to
+ # system fonts when it can't find the font file in the cobalt content.
+ 'cobalt_font_package': 'android_system',
+
+ # On Android, we almost never want to actually terminate the process, so
+ # instead indicate that we would instead like to be suspended when users
+ # initiate an "exit".
+ 'cobalt_user_on_exit_strategy': 'suspend',
+
+ # Switch Android's SurfaceFlinger queue to "async mode" so that we don't
+ # queue up rendered frames which would interfere with frame timing and
+ # more importantly lead to input latency.
+ 'cobalt_egl_swap_interval': 0,
+
+ # Platform-specific implementations to compile into cobalt.
+ 'cobalt_platform_dependencies': [
+ '<(DEPTH)/starboard/android/shared/cobalt/cobalt_platform.gyp:cobalt_platform',
+ ],
+
+ # Platform-specific interfaces to inject into Cobalt's JavaScript 'window'
+ # global object.
+ 'cobalt_webapi_extension_source_idl_files': [
+ 'android.idl',
+ 'feedback_service.idl',
+ ],
+
+ # Platform-specific IDL interface implementations.
+ 'cobalt_webapi_extension_gyp_target':
+ '<(DEPTH)/starboard/android/shared/cobalt/android_webapi_extension.gyp:android_webapi_extension',
+ },
+}
diff --git a/src/starboard/android/shared/cobalt/configuration.py b/src/starboard/android/shared/cobalt/configuration.py
new file mode 100644
index 0000000..8bf09e2
--- /dev/null
+++ b/src/starboard/android/shared/cobalt/configuration.py
@@ -0,0 +1,78 @@
+# Copyright 2017 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Starboard Android Cobalt shared configuration."""
+
+import os
+
+from cobalt.build import cobalt_configuration
+from starboard.tools.testing import test_filter
+
+
+class CobaltAndroidConfiguration(cobalt_configuration.CobaltConfiguration):
+ """Starboard Android Cobalt shared configuration."""
+
+ def __init__(self, platform_configuration, application_name,
+ application_directory):
+ super(CobaltAndroidConfiguration, self).__init__(
+ platform_configuration, application_name, application_directory)
+
+ def GetPostIncludes(self):
+ # If there isn't a configuration.gypi found in the usual place, we'll
+ # supplement with our shared implementation.
+ includes = super(CobaltAndroidConfiguration, self).GetPostIncludes()
+ for include in includes:
+ if os.path.basename(include) == 'configuration.gypi':
+ return includes
+
+ shared_gypi_path = os.path.join(os.path.dirname(__file__),
+ 'configuration.gypi')
+ if os.path.isfile(shared_gypi_path):
+ includes.append(shared_gypi_path)
+ return includes
+
+ def WebdriverBenchmarksEnabled(self):
+ return True
+
+ def GetTestFilters(self):
+ filters = super(CobaltAndroidConfiguration, self).GetTestFilters()
+ for target, tests in self._FAILING_TESTS.iteritems():
+ filters.extend(test_filter.TestFilter(target, test) for test in tests)
+ return filters
+
+ def GetTestEnvVariables(self):
+ return {
+ 'base_unittests': {'ASAN_OPTIONS': 'detect_leaks=0'},
+ 'crypto_unittests': {'ASAN_OPTIONS': 'detect_leaks=0'},
+ 'net_unittests': {'ASAN_OPTIONS': 'detect_leaks=0'}
+ }
+
+ # A map of failing or crashing tests per target.
+ _FAILING_TESTS = {
+ 'net_unittests': [
+ 'DialHttpServerTest.AllOtherRequests',
+ 'DialHttpServerTest.CallbackExceptionInServiceHandler',
+ 'DialHttpServerTest.CallbackHandleRequestReturnsFalse',
+ 'DialHttpServerTest.CallbackNormalTest',
+ 'DialHttpServerTest.CurrentRunningAppRedirect',
+ 'DialHttpServerTest.SendManifest',
+ 'HostResolverImplDnsTest.DnsTaskUnspec',
+ ],
+ 'renderer_test': [
+ # Instead of returning an error when allocating too much texture
+ # memory, Android instead just terminates the process. Since this
+ # test explicitly tries to allocate too much texture memory, we cannot
+ # run it on Android platforms.
+ 'StressTest.TooManyTextures',
+ ],
+ }
diff --git a/src/starboard/android/shared/cobalt/feedback_service.cc b/src/starboard/android/shared/cobalt/feedback_service.cc
new file mode 100644
index 0000000..c17307d
--- /dev/null
+++ b/src/starboard/android/shared/cobalt/feedback_service.cc
@@ -0,0 +1,83 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/cobalt/feedback_service.h"
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+
+namespace cobalt {
+namespace webapi_extension {
+
+void FeedbackService::SendFeedback(
+ const script::ValueHandleHolder& product_specific_data,
+ const std::string& category_tag,
+ script::ExceptionState* exception_state) {
+ const script::Handle<script::ArrayBuffer> empty_array_buffer;
+ SendFeedback(product_specific_data, category_tag, empty_array_buffer,
+ exception_state);
+}
+
+void FeedbackService::SendFeedback(
+ const script::ValueHandleHolder& product_specific_data,
+ const std::string& category_tag,
+ const script::Handle<script::ArrayBuffer>& screenshot_data,
+ script::ExceptionState* exception_state) {
+ using starboard::android::shared::ScopedLocalJavaRef;
+ using starboard::android::shared::JniEnvExt;
+
+ std::unordered_map<std::string, std::string> product_specific_data_map =
+ script::ConvertSimpleObjectToMap(product_specific_data, exception_state);
+
+ JniEnvExt* env = JniEnvExt::Get();
+
+ // Convert the unordered map of product specific data to a hashmap in JNI.
+ ScopedLocalJavaRef<jobject> product_specific_data_hash_map(
+ env->NewObjectOrAbort("java/util/HashMap", "(I)V",
+ product_specific_data_map.size()));
+
+ ScopedLocalJavaRef<jstring> key;
+ ScopedLocalJavaRef<jstring> value;
+
+ for (const auto& data : product_specific_data_map) {
+ key.Reset(env->NewStringStandardUTFOrAbort(data.first.c_str()));
+ value.Reset(env->NewStringStandardUTFOrAbort(data.second.c_str()));
+
+ env->CallObjectMethodOrAbort(
+ product_specific_data_hash_map.Get(), "put",
+ "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", key.Get(),
+ value.Get());
+ }
+
+ ScopedLocalJavaRef<jstring> category_tag_string;
+ category_tag_string.Reset(
+ env->NewStringStandardUTFOrAbort(category_tag.c_str()));
+
+ // Convert the screenshot to a byte array in JNI.
+ ScopedLocalJavaRef<jbyteArray> screenshot_byte_array;
+ if (!screenshot_data.IsEmpty()) {
+ screenshot_byte_array.Reset(env->NewByteArrayFromRaw(
+ reinterpret_cast<const jbyte*>(screenshot_data->Data()),
+ screenshot_data->ByteLength()));
+ env->AbortOnException();
+ }
+
+ env->CallStarboardVoidMethodOrAbort(
+ "sendFeedback", "(Ljava/util/HashMap;Ljava/lang/String;[B)V",
+ product_specific_data_hash_map.Get(), category_tag_string.Get(),
+ screenshot_byte_array.Get());
+}
+
+} // namespace webapi_extension
+} // namespace cobalt
diff --git a/src/starboard/android/shared/cobalt/feedback_service.h b/src/starboard/android/shared/cobalt/feedback_service.h
new file mode 100644
index 0000000..270cf16
--- /dev/null
+++ b/src/starboard/android/shared/cobalt/feedback_service.h
@@ -0,0 +1,49 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_COBALT_FEEDBACK_SERVICE_H_
+#define STARBOARD_ANDROID_SHARED_COBALT_FEEDBACK_SERVICE_H_
+
+#include <string>
+
+#include "cobalt/script/array_buffer.h"
+#include "cobalt/script/exception_state.h"
+#include "cobalt/script/value_handle.h"
+#include "cobalt/script/wrappable.h"
+
+namespace cobalt {
+namespace webapi_extension {
+
+class FeedbackService : public ::cobalt::script::Wrappable {
+ public:
+ void SendFeedback(const script::ValueHandleHolder& product_specific_data,
+ const std::string& category_tag,
+ script::ExceptionState* exception_state);
+
+ void SendFeedback(const script::ValueHandleHolder& product_specific_data,
+ const std::string& category_tag,
+ const script::Handle<script::ArrayBuffer>& screenshot_data,
+ script::ExceptionState* exception_state);
+
+ FeedbackService() = default;
+ FeedbackService(const FeedbackService&) = delete;
+ FeedbackService& operator=(const FeedbackService&) = delete;
+
+ DEFINE_WRAPPABLE_TYPE(FeedbackService);
+};
+
+} // namespace webapi_extension
+} // namespace cobalt
+
+#endif // STARBOARD_ANDROID_SHARED_COBALT_FEEDBACK_SERVICE_H_
diff --git a/src/starboard/android/shared/cobalt/feedback_service.idl b/src/starboard/android/shared/cobalt/feedback_service.idl
new file mode 100644
index 0000000..b4c722a
--- /dev/null
+++ b/src/starboard/android/shared/cobalt/feedback_service.idl
@@ -0,0 +1,21 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// The send feedback report service.
+interface FeedbackService {
+ // Send feedback.
+ [RaisesException] void sendFeedback(any productSpecificData,
+ optional DOMString categoryTag = "",
+ optional ArrayBuffer screenshotData);
+};
diff --git a/src/starboard/android/shared/configuration_public.h b/src/starboard/android/shared/configuration_public.h
new file mode 100644
index 0000000..890d939
--- /dev/null
+++ b/src/starboard/android/shared/configuration_public.h
@@ -0,0 +1,454 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// The Starboard configuration for generic Android. Other devices will have
+// specific Starboard implementations, even if they ultimately are running some
+// version of Android -- but they may base their configuration on the generic
+// Android configuration headers.
+
+// Other source files should never include this header directly, but should
+// include the generic "starboard/configuration.h" instead.
+
+#ifndef STARBOARD_ANDROID_SHARED_CONFIGURATION_PUBLIC_H_
+#define STARBOARD_ANDROID_SHARED_CONFIGURATION_PUBLIC_H_
+
+// The API version implemented by this platform.
+#define SB_API_VERSION SB_EXPERIMENTAL_API_VERSION
+
+// --- Architecture Configuration --------------------------------------------
+
+// Whether the current platform is big endian. SB_IS_LITTLE_ENDIAN will be
+// automatically set based on this.
+#define SB_IS_BIG_ENDIAN 0
+
+// Whether the current platform is a MIPS architecture.
+#define SB_IS_ARCH_MIPS 0
+
+// Whether the current platform is a PPC architecture.
+#define SB_IS_ARCH_PPC 0
+
+// The current platform CPU architecture architecture.
+#if defined(__arm__) || defined(__aarch64__)
+#define SB_IS_ARCH_ARM 1
+#define SB_IS_ARCH_X86 0
+#elif defined(__i386__) || defined(__x86_64__)
+#define SB_IS_ARCH_ARM 0
+#define SB_IS_ARCH_X86 1
+#endif
+
+// Whether the current platform is 32-bit or 64-bit architecture.
+#if defined(__aarch64__) || defined(__x86_64__)
+#define SB_IS_32_BIT 0
+#define SB_IS_64_BIT 1
+#else
+#define SB_IS_32_BIT 1
+#define SB_IS_64_BIT 0
+#endif
+
+// Whether the current platform's pointers are 32-bit.
+// Whether the current platform's longs are 32-bit.
+#if SB_IS(32_BIT)
+#define SB_HAS_32_BIT_POINTERS 1
+#define SB_HAS_32_BIT_LONG 1
+#else
+#define SB_HAS_32_BIT_POINTERS 0
+#define SB_HAS_32_BIT_LONG 0
+#endif
+
+// Whether the current platform's pointers are 64-bit.
+// Whether the current platform's longs are 64-bit.
+#if SB_IS(64_BIT)
+#define SB_HAS_64_BIT_POINTERS 1
+#define SB_HAS_64_BIT_LONG 1
+#else
+#define SB_HAS_64_BIT_POINTERS 0
+#define SB_HAS_64_BIT_LONG 0
+#endif
+
+// Configuration parameters that allow the application to make some general
+// compile-time decisions with respect to the the number of cores likely to be
+// available on this platform. For a definitive measure, the application should
+// still call SbSystemGetNumberOfProcessors at runtime.
+
+// Whether the current platform is expected to have many cores (> 6), or a
+// wildly varying number of cores.
+#define SB_HAS_MANY_CORES 1
+
+// Whether the current platform is expected to have exactly 1 core.
+#define SB_HAS_1_CORE 0
+
+// Whether the current platform is expected to have exactly 2 cores.
+#define SB_HAS_2_CORES 0
+
+// Whether the current platform is expected to have exactly 4 cores.
+#define SB_HAS_4_CORES 0
+
+// Whether the current platform is expected to have exactly 6 cores.
+#define SB_HAS_6_CORES 0
+
+// Whether the current platform's thread scheduler will automatically balance
+// threads between cores, as opposed to systems where threads will only ever run
+// on the specifically pinned core.
+#define SB_HAS_CROSS_CORE_SCHEDULER 1
+
+// Indicates that there is no support for alignment at greater than 16 bytes for
+// items on the stack.
+#define SB_HAS_QUIRK_DOES_NOT_STACK_ALIGN_OVER_16_BYTES 1
+
+// This quirk is used to fix an issue caused by the rewriting of memset to
+// SbMemorySet in third_party/protobuf/src/google/protobuf/stubs/port.h.
+#define SB_HAS_QUIRK_MEMSET_IN_SYSTEM_HEADERS 1
+
+// --- System Header Configuration -------------------------------------------
+
+// Any system headers listed here that are not provided by the platform will be
+// emulated in starboard/types.h.
+
+// Whether the current platform provides the standard header stdarg.h.
+#define SB_HAS_STDARG_H 1
+
+// Whether the current platform provides the standard header stdbool.h.
+#define SB_HAS_STDBOOL_H 1
+
+// Whether the current platform provides the standard header stddef.h.
+#define SB_HAS_STDDEF_H 1
+
+// Whether the current platform provides the standard header stdint.h.
+#define SB_HAS_STDINT_H 1
+
+// Whether the current platform provides the standard header inttypes.h.
+#define SB_HAS_INTTYPES_H 1
+
+// Whether the current platform provides the standard header wchar.h.
+#define SB_HAS_WCHAR_H 1
+
+// Whether the current platform provides the standard header limits.h.
+#define SB_HAS_LIMITS_H 1
+
+// Whether the current platform provides the standard header float.h.
+#define SB_HAS_FLOAT_H 1
+
+// Whether the current platform provides ssize_t.
+#define SB_HAS_SSIZE_T 1
+
+// Type detection for wchar_t.
+#if defined(__WCHAR_MAX__) && \
+ (__WCHAR_MAX__ == 0x7fffffff || __WCHAR_MAX__ == 0xffffffff)
+#define SB_IS_WCHAR_T_UTF32 1
+#elif defined(__WCHAR_MAX__) && \
+ (__WCHAR_MAX__ == 0x7fff || __WCHAR_MAX__ == 0xffff)
+#define SB_IS_WCHAR_T_UTF16 1
+#endif
+
+// Chrome only defines these two if ARMEL or MIPSEL are defined.
+#if defined(__ARMEL__)
+// Chrome has an exclusion for iOS here, we should too when we support iOS.
+#define SB_IS_WCHAR_T_UNSIGNED 1
+#elif defined(__MIPSEL__)
+#define SB_IS_WCHAR_T_SIGNED 1
+#endif
+
+// --- Compiler Configuration ------------------------------------------------
+
+// The platform's annotation for forcing a C function to be inlined.
+#define SB_C_FORCE_INLINE __inline__ __attribute__((always_inline))
+
+// The platform's annotation for marking a C function as suggested to be
+// inlined.
+#define SB_C_INLINE inline
+
+// The platform's annotation for marking a C function as forcibly not
+// inlined.
+#define SB_C_NOINLINE __attribute__((noinline))
+
+// The platform's annotation for marking a symbol as exported outside of the
+// current shared library.
+#define SB_EXPORT_PLATFORM __attribute__((visibility("default")))
+
+// The platform's annotation for marking a symbol as imported from outside of
+// the current linking unit.
+#define SB_IMPORT_PLATFORM
+
+// --- Extensions Configuration ----------------------------------------------
+
+// Do not use <unordered_map> and <unordered_set> for the hash table types.
+#define SB_HAS_STD_UNORDERED_HASH 0
+
+// GCC/Clang doesn't define a long long hash function, except for Android and
+// Game consoles.
+#define SB_HAS_LONG_LONG_HASH 0
+
+// GCC/Clang doesn't define a string hash function, except for Game Consoles.
+#define SB_HAS_STRING_HASH 0
+
+// Desktop Linux needs a using statement for the hash functions.
+#define SB_HAS_HASH_USING 0
+
+// Set this to 1 if hash functions for custom types can be defined as a
+// hash_value() function. Otherwise, they need to be placed inside a
+// partially-specified hash struct template with an operator().
+#define SB_HAS_HASH_VALUE 0
+
+// Set this to 1 if use of hash_map or hash_set causes a deprecation warning
+// (which then breaks the build).
+#define SB_HAS_HASH_WARNING 1
+
+// The location to include hash_map on this platform.
+#define SB_HASH_MAP_INCLUDE <ext/hash_map>
+
+// C++'s hash_map and hash_set are often found in different namespaces depending
+// on the compiler.
+#define SB_HASH_NAMESPACE __gnu_cxx
+
+// The location to include hash_set on this platform.
+#define SB_HASH_SET_INCLUDE <ext/hash_set>
+
+// Define this to how this platform copies varargs blocks.
+#define SB_VA_COPY(dest, source) va_copy(dest, source)
+
+// --- Filesystem Configuration ----------------------------------------------
+
+// The current platform's maximum length of the name of a single directory
+// entry, not including the absolute path.
+#define SB_FILE_MAX_NAME 64
+
+// The current platform's maximum length of an absolute path.
+#define SB_FILE_MAX_PATH 4096
+
+// The current platform's maximum number of files that can be opened at the
+// same time by one process.
+#define SB_FILE_MAX_OPEN 256
+
+// The current platform's file path component separator character. This is the
+// character that appears after a directory in a file path. For example, the
+// absolute canonical path of the file "/path/to/a/file.txt" uses '/' as a path
+// component separator character.
+#define SB_FILE_SEP_CHAR '/'
+
+// The current platform's alternate file path component separator character.
+// This is like SB_FILE_SEP_CHAR, except if your platform supports an alternate
+// character, then you can place that here. For example, on windows machines,
+// the primary separator character is probably '\', but the alternate is '/'.
+#define SB_FILE_ALT_SEP_CHAR '/'
+
+// The current platform's search path component separator character. When
+// specifying an ordered list of absolute paths of directories to search for a
+// given reason, this is the character that appears between entries. For
+// example, the search path of "/etc/search/first:/etc/search/second" uses ':'
+// as a search path component separator character.
+#define SB_PATH_SEP_CHAR ':'
+
+// The string form of SB_FILE_SEP_CHAR.
+#define SB_FILE_SEP_STRING "/"
+
+// The string form of SB_FILE_ALT_SEP_CHAR.
+#define SB_FILE_ALT_SEP_STRING "/"
+
+// The string form of SB_PATH_SEP_CHAR.
+#define SB_PATH_SEP_STRING ":"
+
+// --- Graphics Configuration ------------------------------------------------
+
+// Specifies whether this platform supports a performant accelerated blitter
+// API. The basic requirement is a scaled, clipped, alpha-blended blit.
+#define SB_HAS_BLITTER 0
+
+// Specifies the preferred byte order of color channels in a pixel. Refer to
+// starboard/configuration.h for the possible values. EGL/GLES platforms should
+// generally prefer a byte order of RGBA, regardless of endianness.
+#define SB_PREFERRED_RGBA_BYTE_ORDER SB_PREFERRED_RGBA_BYTE_ORDER_RGBA
+
+// Indicates whether or not the given platform supports bilinear filtering.
+// This can be checked to enable/disable renderer tests that verify that this is
+// working properly.
+#define SB_HAS_BILINEAR_FILTERING_SUPPORT 1
+
+// Indicates whether or not the given platform supports rendering of NV12
+// textures. These textures typically originate from video decoders.
+#define SB_HAS_NV12_TEXTURE_SUPPORT 1
+
+// Whether the current platform should frequently flip their display buffer.
+// If this is not required (e.g. SB_MUST_FREQUENTLY_FLIP_DISPLAY_BUFFER is set
+// to 0), then optimizations where the display buffer is not flipped if the
+// scene hasn't changed are enabled.
+#define SB_MUST_FREQUENTLY_FLIP_DISPLAY_BUFFER 0
+
+// --- I/O Configuration -----------------------------------------------------
+
+// Whether the current platform has microphone supported.
+#define SB_HAS_MICROPHONE 1
+
+// Whether the current platform implements the on screen keyboard interface.
+#define SB_HAS_ON_SCREEN_KEYBOARD 0
+
+// Whether the current platform has speech recognizer.
+#define SB_HAS_SPEECH_RECOGNIZER 1
+
+// Whether the current platform has speech synthesis.
+#define SB_HAS_SPEECH_SYNTHESIS 1
+
+// --- Media Configuration ---------------------------------------------------
+
+// Specifies whether this platform supports retrieving system-level closed
+// caption settings
+#define SB_HAS_CAPTIONS 1
+
+// Whether the current platform uses a media player that relies on a URL.
+#define SB_HAS_PLAYER_WITH_URL 0
+
+// The maximum audio bitrate the platform can decode. The following value
+// equals to 5M bytes per seconds which is more than enough for compressed
+// audio.
+#define SB_MEDIA_MAX_AUDIO_BITRATE_IN_BITS_PER_SECOND (40 * 1024 * 1024)
+
+// The maximum video bitrate the platform can decode. The following value
+// equals to 25M bytes per seconds which is more than enough for compressed
+// video.
+#define SB_MEDIA_MAX_VIDEO_BITRATE_IN_BITS_PER_SECOND (200 * 1024 * 1024)
+
+// Specifies whether this platform has webm/vp9 support. This should be set to
+// non-zero on platforms with webm/vp9 support.
+#define SB_HAS_MEDIA_WEBM_VP9_SUPPORT 1
+
+// Specifies whether this platform updates audio frames asynchronously. In such
+// case an extra parameter will be added to |SbAudioSinkConsumeFramesFunc| to
+// indicate the absolute time that the consumed audio frames are reported.
+// Check document for |SbAudioSinkConsumeFramesFunc| in audio_sink.h for more
+// details.
+#define SB_HAS_ASYNC_AUDIO_FRAMES_REPORTING 1
+
+// Specifies the stack size for threads created inside media stack. Set to 0 to
+// use the default thread stack size. Set to non-zero to explicitly set the
+// stack size for media stack threads.
+#define SB_MEDIA_THREAD_STACK_SIZE 0U
+
+// --- Decoder-only Params ---
+
+// Specifies how media buffers must be aligned on this platform as some
+// decoders may have special requirement on the alignment of buffers being
+// decoded.
+#define SB_MEDIA_BUFFER_ALIGNMENT 128U
+
+// Specifies how video frame buffers must be aligned on this platform.
+#define SB_MEDIA_VIDEO_FRAME_ALIGNMENT 256U
+
+// The encoded video frames are compressed in different ways, their decoding
+// time can vary a lot. Occasionally a single frame can take longer time to
+// decode than the average time per frame. The player has to cache some frames
+// to account for such inconsistency. The number of frames being cached are
+// controlled by the following two macros.
+//
+// Specify the number of video frames to be cached before the playback starts.
+// Note that set this value too large may increase the playback start delay.
+#define SB_MEDIA_MAXIMUM_VIDEO_PREROLL_FRAMES 4
+
+// Specify the number of video frames to be cached during playback. A large
+// value leads to more stable fps but also causes the app to use more memory.
+#define SB_MEDIA_MAXIMUM_VIDEO_FRAMES 12
+
+// --- Memory Configuration --------------------------------------------------
+
+// The memory page size, which controls the size of chunks on memory that
+// allocators deal with, and the alignment of those chunks. This doesn't have to
+// be the hardware-defined physical page size, but it should be a multiple of
+// it.
+#define SB_MEMORY_PAGE_SIZE 4096
+
+// Whether this platform has and should use an MMAP function to map physical
+// memory to the virtual address space.
+#define SB_HAS_MMAP 1
+
+// Whether this platform can map executable memory. Implies SB_HAS_MMAP. This is
+// required for platforms that want to JIT.
+#define SB_CAN_MAP_EXECUTABLE_MEMORY 1
+
+// Whether this platform has and should use an growable heap (e.g. with sbrk())
+// to map physical memory to the virtual address space.
+#define SB_HAS_VIRTUAL_REGIONS 0
+
+// Specifies the alignment for IO Buffers, in bytes. Some low-level network APIs
+// may require buffers to have a specific alignment, and this is the place to
+// specify that.
+#define SB_NETWORK_IO_BUFFER_ALIGNMENT 16
+
+// Determines the alignment that allocations should have on this platform.
+#define SB_MALLOC_ALIGNMENT ((size_t)16U)
+
+// Determines the threshhold of allocation size that should be done with mmap
+// (if available), rather than allocated within the core heap.
+#define SB_DEFAULT_MMAP_THRESHOLD ((size_t)(256 * 1024U))
+
+// Defines the path where memory debugging logs should be written to.
+#define SB_MEMORY_LOG_PATH "/tmp/starboard"
+
+// --- Network Configuration -------------------------------------------------
+
+// Specifies whether this platform supports IPV6.
+#define SB_HAS_IPV6 1
+
+// Specifies whether this platform supports pipe.
+#define SB_HAS_PIPE 1
+
+// --- Timing API ------------------------------------------------------------
+
+// Whether this platform has an API to retrieve how long the current thread
+// has spent in the executing state.
+#define SB_HAS_TIME_THREAD_NOW 1
+
+// --- Thread Configuration --------------------------------------------------
+
+// Whether the current platform supports thread priorities.
+#define SB_HAS_THREAD_PRIORITY_SUPPORT 1
+
+// Defines the maximum number of simultaneous threads for this platform. Some
+// platforms require sharing thread handles with other kinds of system handles,
+// like mutexes, so we want to keep this managable.
+#define SB_MAX_THREADS 90
+
+// The maximum number of thread local storage keys supported by this platform.
+// This comes from bionic PTHREAD_KEYS_MAX in limits.h, which we've decided
+// to not include here to decrease symbol pollution.
+#define SB_MAX_THREAD_LOCAL_KEYS 128
+
+// The maximum length of the name for a thread, including the NULL-terminator.
+#define SB_MAX_THREAD_NAME_LENGTH 16
+
+// --- Tuneable Parameters ---------------------------------------------------
+
+// Specifies the network receive buffer size in bytes, set via
+// SbSocketSetReceiveBufferSize().
+//
+// Setting this to 0 indicates that SbSocketSetReceiveBufferSize() should
+// not be called. Use this for OSs (such as Linux) where receive buffer
+// auto-tuning is better.
+//
+// On some platforms, this may affect max TCP window size which may
+// dramatically affect throughput in the presence of latency.
+//
+// If your platform does not have a good TCP auto-tuning mechanism,
+// a setting of (128 * 1024) here is recommended.
+#define SB_NETWORK_RECEIVE_BUFFER_SIZE (0)
+
+// --- User Configuration ----------------------------------------------------
+
+// The maximum number of users that can be signed in at the same time.
+#define SB_USER_MAX_SIGNED_IN 1
+
+// --- Platform Specific Audits ----------------------------------------------
+
+#if !defined(__GNUC__)
+#error "Android builds need a GCC-like compiler (for the moment)."
+#endif
+
+#endif // STARBOARD_ANDROID_SHARED_CONFIGURATION_PUBLIC_H_
diff --git a/src/starboard/android/shared/decode_target_create.cc b/src/starboard/android/shared/decode_target_create.cc
new file mode 100644
index 0000000..9b6f900
--- /dev/null
+++ b/src/starboard/android/shared/decode_target_create.cc
@@ -0,0 +1,148 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/decode_target_create.h"
+
+#include <android/native_window_jni.h>
+#include <jni.h>
+
+#include <EGL/egl.h>
+#include <GLES2/gl2.h>
+#include <GLES2/gl2ext.h>
+
+#include "starboard/android/shared/decode_target_internal.h"
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/decode_target.h"
+#include "starboard/shared/gles/gl_call.h"
+
+using starboard::android::shared::JniEnvExt;
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+namespace {
+jobject CreateSurfaceTexture(int gl_texture_id) {
+ JniEnvExt* env = JniEnvExt::Get();
+
+ jobject local_surface_texture = env->NewObjectOrAbort(
+ "android/graphics/SurfaceTexture", "(I)V", gl_texture_id);
+
+ jobject global_surface_texture =
+ env->ConvertLocalRefToGlobalRef(local_surface_texture);
+
+ return global_surface_texture;
+}
+
+jobject CreateSurfaceFromSurfaceTexture(jobject surface_texture) {
+ JniEnvExt* env = JniEnvExt::Get();
+
+ jobject local_surface = env->NewObjectOrAbort(
+ "android/view/Surface", "(Landroid/graphics/SurfaceTexture;)V",
+ surface_texture);
+
+ jobject global_surface = env->ConvertLocalRefToGlobalRef(local_surface);
+
+ return global_surface;
+}
+
+struct CreateParams {
+ int width;
+ int height;
+ SbDecodeTargetFormat format;
+
+ SbDecodeTarget decode_target_out;
+};
+
+void CreateWithContextRunner(void* context) {
+ CreateParams* params = static_cast<CreateParams*>(context);
+
+ // Setup the GL texture that Android's MediaCodec library will target with
+ // the decoder. We don't call glTexImage2d() on it, Android will handle
+ // the creation of the content when SurfaceTexture::updateTexImage() is
+ // called.
+ GLuint texture;
+ GL_CALL(glGenTextures(1, &texture));
+ GL_CALL(glBindTexture(GL_TEXTURE_EXTERNAL_OES, texture));
+ GL_CALL(glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER,
+ GL_LINEAR));
+ GL_CALL(glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER,
+ GL_LINEAR));
+ GL_CALL(glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_S,
+ GL_CLAMP_TO_EDGE));
+ GL_CALL(glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_T,
+ GL_CLAMP_TO_EDGE));
+
+ SbDecodeTarget decode_target = new SbDecodeTargetPrivate;
+ decode_target->data = new SbDecodeTargetPrivate::Data;
+
+ // Wrap the GL texture in an Android SurfaceTexture object.
+ decode_target->data->surface_texture = CreateSurfaceTexture(texture);
+
+ // We will also need an Android Surface object in order to obtain a
+ // ANativeWindow object that we can pass into the AMediaCodec library.
+ decode_target->data->surface =
+ CreateSurfaceFromSurfaceTexture(decode_target->data->surface_texture);
+
+ decode_target->data->native_window =
+ ANativeWindow_fromSurface(JniEnvExt::Get(), decode_target->data->surface);
+
+ // Setup our publicly accessible decode target information.
+ decode_target->data->info.format = params->format;
+ decode_target->data->info.is_opaque = true;
+ decode_target->data->info.width = params->width;
+ decode_target->data->info.height = params->height;
+ decode_target->data->info.planes[0].texture = texture;
+ decode_target->data->info.planes[0].gl_texture_target =
+ GL_TEXTURE_EXTERNAL_OES;
+ decode_target->data->info.planes[0].width = params->width;
+ decode_target->data->info.planes[0].height = params->height;
+
+ // These values will be initialized when SbPlayerGetCurrentFrame() is called.
+ decode_target->data->info.planes[0].content_region.left = 0;
+ decode_target->data->info.planes[0].content_region.right = 0;
+ decode_target->data->info.planes[0].content_region.top = 0;
+ decode_target->data->info.planes[0].content_region.bottom = 0;
+
+ GL_CALL(glBindTexture(GL_TEXTURE_EXTERNAL_OES, 0));
+
+ params->decode_target_out = decode_target;
+}
+
+} // namespace
+
+SbDecodeTarget DecodeTargetCreate(
+ SbDecodeTargetGraphicsContextProvider* provider,
+ SbDecodeTargetFormat format,
+ int width,
+ int height) {
+ SB_DCHECK(format == kSbDecodeTargetFormat1PlaneRGBA);
+ if (format != kSbDecodeTargetFormat1PlaneRGBA) {
+ return kSbDecodeTargetInvalid;
+ }
+
+ CreateParams params;
+ params.width = width;
+ params.height = height;
+ params.format = format;
+ params.decode_target_out = kSbDecodeTargetInvalid;
+
+ SbDecodeTargetRunInGlesContext(
+ provider, &CreateWithContextRunner, ¶ms);
+ return params.decode_target_out;
+}
+
+} // namespace shared
+} // namespace android
+} // namespace starboard
diff --git a/src/starboard/android/shared/decode_target_create.h b/src/starboard/android/shared/decode_target_create.h
new file mode 100644
index 0000000..3f32621
--- /dev/null
+++ b/src/starboard/android/shared/decode_target_create.h
@@ -0,0 +1,34 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_DECODE_TARGET_CREATE_H_
+#define STARBOARD_ANDROID_SHARED_DECODE_TARGET_CREATE_H_
+
+#include "starboard/decode_target.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+SbDecodeTarget DecodeTargetCreate(
+ SbDecodeTargetGraphicsContextProvider* provider,
+ SbDecodeTargetFormat format,
+ int width,
+ int height);
+
+} // namespace shared
+} // namespace android
+} // namespace starboard
+
+#endif // STARBOARD_ANDROID_SHARED_DECODE_TARGET_CREATE_H_
diff --git a/src/starboard/android/shared/decode_target_get_info.cc b/src/starboard/android/shared/decode_target_get_info.cc
new file mode 100644
index 0000000..38ded1a
--- /dev/null
+++ b/src/starboard/android/shared/decode_target_get_info.cc
@@ -0,0 +1,29 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/decode_target_internal.h"
+#include "starboard/decode_target.h"
+#include "starboard/memory.h"
+
+bool SbDecodeTargetGetInfo(SbDecodeTarget decode_target,
+ SbDecodeTargetInfo* out_info) {
+ if (!SbMemoryIsZero(out_info, sizeof(*out_info))) {
+ SB_DCHECK(false) << "out_info must be zeroed out.";
+ return false;
+ }
+
+ SbMemoryCopy(out_info, &decode_target->data->info, sizeof(*out_info));
+
+ return true;
+}
diff --git a/src/starboard/android/shared/decode_target_internal.cc b/src/starboard/android/shared/decode_target_internal.cc
new file mode 100644
index 0000000..957fb1d
--- /dev/null
+++ b/src/starboard/android/shared/decode_target_internal.cc
@@ -0,0 +1,30 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/decode_target_internal.h"
+
+#include "starboard/android/shared/jni_env_ext.h"
+
+using starboard::android::shared::JniEnvExt;
+
+SbDecodeTargetPrivate::Data::~Data() {
+ ANativeWindow_release(native_window);
+
+ JniEnvExt* env = JniEnvExt::Get();
+ env->DeleteGlobalRef(surface);
+ env->DeleteGlobalRef(surface_texture);
+
+ glDeleteTextures(1, &info.planes[0].texture);
+ SB_DCHECK(glGetError() == GL_NO_ERROR);
+}
diff --git a/src/starboard/android/shared/decode_target_internal.h b/src/starboard/android/shared/decode_target_internal.h
new file mode 100644
index 0000000..953840a
--- /dev/null
+++ b/src/starboard/android/shared/decode_target_internal.h
@@ -0,0 +1,47 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_DECODE_TARGET_INTERNAL_H_
+#define STARBOARD_ANDROID_SHARED_DECODE_TARGET_INTERNAL_H_
+
+#include <android/native_window.h>
+#include <GLES2/gl2.h>
+#include <jni.h>
+
+#include "starboard/common/ref_counted.h"
+#include "starboard/decode_target.h"
+
+struct SbDecodeTargetPrivate {
+ class Data : public starboard::RefCounted<Data> {
+ public:
+ Data() {}
+
+ // Java objects which wrap the texture. We hold on to global references
+ // to these objects.
+ jobject surface_texture;
+ jobject surface;
+ ANativeWindow* native_window;
+
+ // Publicly accessible information about the decode target.
+ SbDecodeTargetInfo info;
+
+ private:
+ friend class starboard::RefCounted<Data>;
+ ~Data();
+ };
+
+ starboard::scoped_refptr<Data> data;
+};
+
+#endif // STARBOARD_ANDROID_SHARED_DECODE_TARGET_INTERNAL_H_
diff --git a/src/starboard/android/shared/decode_target_release.cc b/src/starboard/android/shared/decode_target_release.cc
new file mode 100644
index 0000000..29a3f4c
--- /dev/null
+++ b/src/starboard/android/shared/decode_target_release.cc
@@ -0,0 +1,28 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <GLES2/gl2.h>
+
+#include "starboard/android/shared/decode_target_internal.h"
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/decode_target.h"
+#include "starboard/log.h"
+
+void SbDecodeTargetRelease(SbDecodeTarget decode_target) {
+ // Most of the actual data within |decode_target| is stored in the reference
+ // counted decode_target->data, so deleting |decode_target| here may not
+ // actually release any resources, if there are other references to
+ // decode_target->data.
+ delete decode_target;
+}
diff --git a/src/starboard/android/shared/directory_close.cc b/src/starboard/android/shared/directory_close.cc
new file mode 100644
index 0000000..30d52ec
--- /dev/null
+++ b/src/starboard/android/shared/directory_close.cc
@@ -0,0 +1,32 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/directory.h"
+
+#include <android/asset_manager.h>
+
+#include "starboard/android/shared/file_internal.h"
+
+#include "starboard/android/shared/directory_internal.h"
+#include "starboard/shared/iso/impl/directory_close.h"
+
+bool SbDirectoryClose(SbDirectory directory) {
+ if (directory && directory->asset_dir) {
+ AAssetDir_close(directory->asset_dir);
+ delete directory;
+ return true;
+ }
+
+ return ::starboard::shared::iso::impl::SbDirectoryClose(directory);
+}
diff --git a/src/starboard/android/shared/directory_get_next.cc b/src/starboard/android/shared/directory_get_next.cc
new file mode 100644
index 0000000..f1e5b02
--- /dev/null
+++ b/src/starboard/android/shared/directory_get_next.cc
@@ -0,0 +1,35 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/directory.h"
+
+#include <android/asset_manager.h>
+
+#include "starboard/android/shared/directory_internal.h"
+#include "starboard/shared/iso/impl/directory_get_next.h"
+
+bool SbDirectoryGetNext(SbDirectory directory, SbDirectoryEntry* out_entry) {
+ if (directory && directory->asset_dir && out_entry) {
+ const char* file_name = AAssetDir_getNextFileName(directory->asset_dir);
+ if (file_name == NULL) {
+ return false;
+ }
+ size_t size = SB_ARRAY_SIZE_INT(out_entry->name);
+ SbStringCopy(out_entry->name, file_name, size);
+ return true;
+ }
+
+ return ::starboard::shared::iso::impl::SbDirectoryGetNext(directory,
+ out_entry);
+}
diff --git a/src/starboard/android/shared/directory_internal.h b/src/starboard/android/shared/directory_internal.h
new file mode 100644
index 0000000..1e2108b
--- /dev/null
+++ b/src/starboard/android/shared/directory_internal.h
@@ -0,0 +1,37 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_DIRECTORY_INTERNAL_H_
+#define STARBOARD_ANDROID_SHARED_DIRECTORY_INTERNAL_H_
+
+#include <android/asset_manager.h>
+
+#include <dirent.h>
+
+#include "starboard/directory.h"
+#include "starboard/shared/internal_only.h"
+
+struct SbDirectoryPrivate {
+ // Note: Only one of these two fields will be valid for any given file.
+
+ // The ISO C directory stream handle, or NULL if it's an asset directory.
+ DIR* directory;
+
+ // If not NULL this is an Android asset directory.
+ AAssetDir* asset_dir;
+
+ SbDirectoryPrivate() : directory(NULL), asset_dir(NULL) {}
+};
+
+#endif // STARBOARD_ANDROID_SHARED_DIRECTORY_INTERNAL_H_
diff --git a/src/starboard/android/shared/directory_open.cc b/src/starboard/android/shared/directory_open.cc
new file mode 100644
index 0000000..9c0f7a8
--- /dev/null
+++ b/src/starboard/android/shared/directory_open.cc
@@ -0,0 +1,43 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/directory.h"
+
+#include <android/asset_manager.h>
+
+#include "starboard/android/shared/file_internal.h"
+
+#include "starboard/android/shared/directory_internal.h"
+#include "starboard/shared/iso/impl/directory_open.h"
+
+using starboard::android::shared::IsAndroidAssetPath;
+using starboard::android::shared::OpenAndroidAssetDir;
+
+SbDirectory SbDirectoryOpen(const char* path, SbFileError* out_error) {
+ if (!IsAndroidAssetPath(path)) {
+ return ::starboard::shared::iso::impl::SbDirectoryOpen(path, out_error);
+ }
+
+ AAssetDir* asset_dir = OpenAndroidAssetDir(path);
+ if (asset_dir) {
+ SbDirectory result = new SbDirectoryPrivate();
+ result->asset_dir = asset_dir;
+ return result;
+ }
+
+ if (out_error) {
+ *out_error = kSbFileErrorFailed;
+ }
+ return kSbDirectoryInvalid;
+}
diff --git a/src/starboard/android/shared/drm_create_system.cc b/src/starboard/android/shared/drm_create_system.cc
new file mode 100644
index 0000000..e92d6a1
--- /dev/null
+++ b/src/starboard/android/shared/drm_create_system.cc
@@ -0,0 +1,48 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/drm_system.h"
+
+#include "starboard/android/shared/media_common.h"
+
+SbDrmSystem SbDrmCreateSystem(
+ const char* key_system,
+ void* context,
+ SbDrmSessionUpdateRequestFunc update_request_callback,
+ SbDrmSessionUpdatedFunc session_updated_callback,
+ SbDrmSessionKeyStatusesChangedFunc key_statuses_changed_callback,
+ SbDrmServerCertificateUpdatedFunc server_certificate_updated_callback,
+ SbDrmSessionClosedFunc session_closed_callback) {
+ using starboard::android::shared::DrmSystem;
+ using starboard::android::shared::IsWidevine;
+
+ if (!update_request_callback || !session_updated_callback ||
+ !key_statuses_changed_callback || !server_certificate_updated_callback ||
+ !session_closed_callback) {
+ return kSbDrmSystemInvalid;
+ }
+
+ if (!IsWidevine(key_system)) {
+ return kSbDrmSystemInvalid;
+ }
+
+ DrmSystem* drm_system =
+ new DrmSystem(context, update_request_callback, session_updated_callback,
+ key_statuses_changed_callback);
+ if (!drm_system->is_valid()) {
+ delete drm_system;
+ return kSbDrmSystemInvalid;
+ }
+ return drm_system;
+}
diff --git a/src/starboard/android/shared/drm_system.cc b/src/starboard/android/shared/drm_system.cc
new file mode 100644
index 0000000..e01c8f7
--- /dev/null
+++ b/src/starboard/android/shared/drm_system.cc
@@ -0,0 +1,307 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/drm_system.h"
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/android/shared/media_common.h"
+
+namespace {
+
+using starboard::android::shared::DrmSystem;
+using starboard::android::shared::JniEnvExt;
+using starboard::android::shared::ScopedLocalJavaRef;
+
+const char kNoUrl[] = "";
+
+// Using all capital names to be consistent with other Android media statuses.
+// They are defined in the same order as in their Java counterparts. Their
+// values should be kept in consistent with their Java counterparts defined in
+// android.media.MediaDrm.KeyStatus.
+const jint MEDIA_DRM_KEY_STATUS_EXPIRED = 1;
+const jint MEDIA_DRM_KEY_STATUS_INTERNAL_ERROR = 4;
+const jint MEDIA_DRM_KEY_STATUS_OUTPUT_NOT_ALLOWED = 2;
+const jint MEDIA_DRM_KEY_STATUS_PENDING = 3;
+const jint MEDIA_DRM_KEY_STATUS_USABLE = 0;
+
+} // namespace
+
+extern "C" SB_EXPORT_PLATFORM void
+Java_dev_cobalt_media_MediaDrmBridge_nativeOnSessionMessage(
+ JNIEnv* env,
+ jobject unused_this,
+ jlong native_media_drm_bridge,
+ jint ticket,
+ jbyteArray j_session_id,
+ jint request_type,
+ jbyteArray j_message) {
+ jbyte* session_id_elements = env->GetByteArrayElements(j_session_id, NULL);
+ jsize session_id_size = env->GetArrayLength(j_session_id);
+
+ jbyte* message_elements = env->GetByteArrayElements(j_message, NULL);
+ jsize message_size = env->GetArrayLength(j_message);
+
+ SB_DCHECK(session_id_elements);
+ SB_DCHECK(message_elements);
+
+ DrmSystem* drm_system = reinterpret_cast<DrmSystem*>(native_media_drm_bridge);
+ SB_DCHECK(drm_system);
+ drm_system->CallUpdateRequestCallback(ticket, session_id_elements,
+ session_id_size, message_elements,
+ message_size, kNoUrl);
+ env->ReleaseByteArrayElements(j_session_id, session_id_elements, JNI_ABORT);
+ env->ReleaseByteArrayElements(j_message, message_elements, JNI_ABORT);
+}
+
+extern "C" SB_EXPORT_PLATFORM void
+Java_dev_cobalt_media_MediaDrmBridge_nativeOnKeyStatusChange(
+ JniEnvExt* env,
+ jobject unused_this,
+ jlong native_media_drm_bridge,
+ jbyteArray j_session_id,
+ jobjectArray j_key_status_array) {
+ jbyte* session_id_elements = env->GetByteArrayElements(j_session_id, NULL);
+ jsize session_id_size = env->GetArrayLength(j_session_id);
+
+ SB_DCHECK(session_id_elements);
+
+ // NULL array indicates key status isn't supported (i.e. Android API < 23)
+ jsize length = (j_key_status_array == NULL) ? 0
+ : env->GetArrayLength(j_key_status_array);
+ std::vector<SbDrmKeyId> drm_key_ids(length);
+ std::vector<SbDrmKeyStatus> drm_key_statuses(length);
+
+ for (jsize i = 0; i < length; ++i) {
+ jobject j_key_status =
+ env->GetObjectArrayElementOrAbort(j_key_status_array, i);
+ jbyteArray j_key_id = static_cast<jbyteArray>(
+ env->CallObjectMethodOrAbort(j_key_status, "getKeyId", "()[B"));
+
+ jbyte* key_id_elements = env->GetByteArrayElements(j_key_id, NULL);
+ jsize key_id_size = env->GetArrayLength(j_key_id);
+ SB_DCHECK(key_id_elements);
+
+ SB_DCHECK(key_id_size <= sizeof(drm_key_ids[i].identifier));
+ SbMemoryCopy(drm_key_ids[i].identifier, key_id_elements, key_id_size);
+ env->ReleaseByteArrayElements(j_key_id, key_id_elements, JNI_ABORT);
+ drm_key_ids[i].identifier_size = key_id_size;
+
+ jint j_status_code =
+ env->CallIntMethodOrAbort(j_key_status, "getStatusCode", "()I");
+ if (j_status_code == MEDIA_DRM_KEY_STATUS_EXPIRED) {
+ drm_key_statuses[i] = kSbDrmKeyStatusExpired;
+ } else if (j_status_code == MEDIA_DRM_KEY_STATUS_INTERNAL_ERROR) {
+ drm_key_statuses[i] = kSbDrmKeyStatusError;
+ } else if (j_status_code == MEDIA_DRM_KEY_STATUS_OUTPUT_NOT_ALLOWED) {
+ drm_key_statuses[i] = kSbDrmKeyStatusRestricted;
+ } else if (j_status_code == MEDIA_DRM_KEY_STATUS_PENDING) {
+ drm_key_statuses[i] = kSbDrmKeyStatusPending;
+ } else if (j_status_code == MEDIA_DRM_KEY_STATUS_USABLE) {
+ drm_key_statuses[i] = kSbDrmKeyStatusUsable;
+ } else {
+ SB_NOTREACHED();
+ drm_key_statuses[i] = kSbDrmKeyStatusError;
+ }
+ }
+
+ DrmSystem* drm_system = reinterpret_cast<DrmSystem*>(native_media_drm_bridge);
+ SB_DCHECK(drm_system);
+ drm_system->CallDrmSessionKeyStatusesChangedCallback(
+ session_id_elements, session_id_size, drm_key_ids, drm_key_statuses);
+
+ env->ReleaseByteArrayElements(j_session_id, session_id_elements, JNI_ABORT);
+}
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+namespace {
+
+jbyteArray ByteArrayFromRaw(const void* data, int size) {
+ return JniEnvExt::Get()->NewByteArrayFromRaw(static_cast<const jbyte*>(data),
+ size);
+}
+
+} // namespace
+
+DrmSystem::DrmSystem(
+ void* context,
+ SbDrmSessionUpdateRequestFunc update_request_callback,
+ SbDrmSessionUpdatedFunc session_updated_callback,
+ SbDrmSessionKeyStatusesChangedFunc key_statuses_changed_callback)
+ : context_(context),
+ update_request_callback_(update_request_callback),
+ session_updated_callback_(session_updated_callback),
+ key_statuses_changed_callback_(key_statuses_changed_callback),
+ j_media_drm_bridge_(NULL),
+ j_media_crypto_(NULL),
+ hdcp_lost_(false) {
+ JniEnvExt* env = JniEnvExt::Get();
+ j_media_drm_bridge_ = env->CallStaticObjectMethodOrAbort(
+ "dev/cobalt/media/MediaDrmBridge", "create",
+ "(J)Ldev/cobalt/media/MediaDrmBridge;", reinterpret_cast<jlong>(this));
+ if (!j_media_drm_bridge_) {
+ SB_LOG(ERROR) << "Failed to create MediaDrmBridge.";
+ return;
+ }
+ j_media_drm_bridge_ = env->ConvertLocalRefToGlobalRef(j_media_drm_bridge_);
+ j_media_crypto_ = env->CallObjectMethodOrAbort(
+ j_media_drm_bridge_, "getMediaCrypto", "()Landroid/media/MediaCrypto;");
+ if (!j_media_crypto_) {
+ SB_LOG(ERROR) << "Failed to create MediaCrypto.";
+ return;
+ }
+ j_media_crypto_ = env->ConvertLocalRefToGlobalRef(j_media_crypto_);
+}
+
+DrmSystem::~DrmSystem() {
+ JniEnvExt* env = JniEnvExt::Get();
+ if (j_media_crypto_) {
+ env->DeleteGlobalRef(j_media_crypto_);
+ j_media_crypto_ = NULL;
+ }
+ if (j_media_drm_bridge_) {
+ env->CallVoidMethodOrAbort(j_media_drm_bridge_, "destroy", "()V");
+ env->DeleteGlobalRef(j_media_drm_bridge_);
+ j_media_drm_bridge_ = NULL;
+ }
+}
+
+void DrmSystem::GenerateSessionUpdateRequest(int ticket,
+ const char* type,
+ const void* initialization_data,
+ int initialization_data_size) {
+ ScopedLocalJavaRef<jbyteArray> j_init_data(
+ ByteArrayFromRaw(initialization_data, initialization_data_size));
+ JniEnvExt* env = JniEnvExt::Get();
+ ScopedLocalJavaRef<jstring> j_mime(env->NewStringStandardUTFOrAbort(type));
+ env->CallVoidMethodOrAbort(
+ j_media_drm_bridge_, "createSession", "(I[BLjava/lang/String;)V",
+ static_cast<jint>(ticket), j_init_data.Get(), j_mime.Get());
+ // |update_request_callback_| will be called by Java calling into
+ // |onSessionMessage|.
+}
+
+void DrmSystem::UpdateSession(int ticket,
+ const void* key,
+ int key_size,
+ const void* session_id,
+ int session_id_size) {
+ ScopedLocalJavaRef<jbyteArray> j_session_id(
+ ByteArrayFromRaw(session_id, session_id_size));
+ ScopedLocalJavaRef<jbyteArray> j_response(ByteArrayFromRaw(key, key_size));
+
+ jboolean status = JniEnvExt::Get()->CallBooleanMethodOrAbort(
+ j_media_drm_bridge_, "updateSession", "([B[B)Z", j_session_id.Get(),
+ j_response.Get());
+ session_updated_callback_(
+ this, context_, ticket,
+ status == JNI_TRUE ? kSbDrmStatusSuccess : kSbDrmStatusUnknownError, NULL,
+ session_id, session_id_size);
+}
+
+void DrmSystem::CloseSession(const void* session_id, int session_id_size) {
+ JniEnvExt* env = JniEnvExt::Get();
+ ScopedLocalJavaRef<jbyteArray> j_session_id(
+ ByteArrayFromRaw(session_id, session_id_size));
+ std::string session_id_as_string(
+ static_cast<const char*>(session_id),
+ static_cast<const char*>(session_id) + session_id_size);
+
+ {
+ ScopedLock scoped_lock(mutex_);
+ auto iter = cached_drm_key_ids_.find(session_id_as_string);
+ if (iter != cached_drm_key_ids_.end()) {
+ cached_drm_key_ids_.erase(iter);
+ }
+ }
+ env->CallVoidMethodOrAbort(j_media_drm_bridge_, "closeSession", "([B)V",
+ j_session_id.Get());
+}
+
+DrmSystem::DecryptStatus DrmSystem::Decrypt(InputBuffer* buffer) {
+ SB_DCHECK(buffer);
+ SB_DCHECK(buffer->drm_info());
+ SB_DCHECK(j_media_crypto_);
+ // The actual decryption will take place by calling |queueSecureInputBuffer|
+ // in the decoders. Our existence implies that there is enough information
+ // to perform the decryption.
+ // TODO: Returns kRetry when |UpdateSession| is not called at all to allow the
+ // player worker to handle the retry logic.
+ return kSuccess;
+}
+
+void DrmSystem::CallUpdateRequestCallback(int ticket,
+ const void* session_id,
+ int session_id_size,
+ const void* content,
+ int content_size,
+ const char* url) {
+ update_request_callback_(this, context_, ticket, kSbDrmStatusSuccess,
+ kSbDrmSessionRequestTypeLicenseRequest, NULL,
+ session_id, session_id_size, content, content_size,
+ url);
+}
+
+void DrmSystem::CallDrmSessionKeyStatusesChangedCallback(
+ const void* session_id,
+ int session_id_size,
+ const std::vector<SbDrmKeyId>& drm_key_ids,
+ const std::vector<SbDrmKeyStatus>& drm_key_statuses) {
+ SB_DCHECK(drm_key_ids.size() == drm_key_statuses.size());
+
+ std::string session_id_as_string(
+ static_cast<const char*>(session_id),
+ static_cast<const char*>(session_id) + session_id_size);
+
+ bool hdcp_lost = false;
+ {
+ ScopedLock scoped_lock(mutex_);
+ cached_drm_key_ids_[session_id_as_string] = drm_key_ids;
+ hdcp_lost = hdcp_lost_;
+ }
+
+ if (hdcp_lost) {
+ OnInsufficientOutputProtection();
+ return;
+ }
+
+ key_statuses_changed_callback_(this, context_, session_id, session_id_size,
+ static_cast<int>(drm_key_ids.size()),
+ drm_key_ids.data(), drm_key_statuses.data());
+}
+
+void DrmSystem::OnInsufficientOutputProtection() {
+ // HDCP has lost, update the statuses of all keys in all known sessions to be
+ // restricted.
+ ScopedLock scoped_lock(mutex_);
+ hdcp_lost_ = true;
+ for (auto& iter : cached_drm_key_ids_) {
+ const std::string& session_id = iter.first;
+ const std::vector<SbDrmKeyId>& drm_key_ids = iter.second;
+ std::vector<SbDrmKeyStatus> drm_key_statuses(drm_key_ids.size(),
+ kSbDrmKeyStatusRestricted);
+
+ key_statuses_changed_callback_(this, context_, session_id.data(),
+ session_id.size(),
+ static_cast<int>(drm_key_ids.size()),
+ drm_key_ids.data(), drm_key_statuses.data());
+ }
+}
+
+} // namespace shared
+} // namespace android
+} // namespace starboard
diff --git a/src/starboard/android/shared/drm_system.h b/src/starboard/android/shared/drm_system.h
new file mode 100644
index 0000000..cec052b
--- /dev/null
+++ b/src/starboard/android/shared/drm_system.h
@@ -0,0 +1,99 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_DRM_SYSTEM_H_
+#define STARBOARD_ANDROID_SHARED_DRM_SYSTEM_H_
+
+#include "starboard/shared/starboard/drm/drm_system_internal.h"
+
+#include <jni.h>
+
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include "starboard/log.h"
+#include "starboard/mutex.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+class DrmSystem : public ::SbDrmSystemPrivate {
+ public:
+ DrmSystem(void* context,
+ SbDrmSessionUpdateRequestFunc update_request_callback,
+ SbDrmSessionUpdatedFunc session_updated_callback,
+ SbDrmSessionKeyStatusesChangedFunc key_statuses_changed_callback);
+
+ ~DrmSystem() override;
+ void GenerateSessionUpdateRequest(int ticket,
+ const char* type,
+ const void* initialization_data,
+ int initialization_data_size) override;
+ void UpdateSession(int ticket,
+ const void* key,
+ int key_size,
+ const void* session_id,
+ int session_id_size);
+ void CloseSession(const void* session_id, int session_id_size) override;
+ DecryptStatus Decrypt(InputBuffer* buffer) override;
+#if SB_API_VERSION >= 10
+ void UpdateServerCertificate(int ticket,
+ const void* certificate,
+ int certificate_size) override {
+ SB_UNREFERENCED_PARAMETER(ticket);
+ SB_UNREFERENCED_PARAMETER(certificate);
+ SB_UNREFERENCED_PARAMETER(certificate_size);
+ }
+#endif // SB_API_VERSION >= 10
+
+ jobject GetMediaCrypto() const { return j_media_crypto_; }
+ void CallUpdateRequestCallback(int ticket,
+ const void* session_id,
+ int session_id_size,
+ const void* content,
+ int content_size,
+ const char* url);
+ void CallDrmSessionKeyStatusesChangedCallback(
+ const void* session_id,
+ int session_id_size,
+ const std::vector<SbDrmKeyId>& drm_key_ids,
+ const std::vector<SbDrmKeyStatus>& drm_key_statuses);
+ void OnInsufficientOutputProtection();
+
+ bool is_valid() const {
+ return j_media_drm_bridge_ != NULL && j_media_crypto_ != NULL;
+ }
+
+ private:
+ void* context_;
+ SbDrmSessionUpdateRequestFunc update_request_callback_;
+ SbDrmSessionUpdatedFunc session_updated_callback_;
+ // TODO: Update key statuses to Cobalt.
+ SbDrmSessionKeyStatusesChangedFunc key_statuses_changed_callback_;
+
+ jobject j_media_drm_bridge_;
+ jobject j_media_crypto_;
+
+ Mutex mutex_;
+ std::unordered_map<std::string, std::vector<SbDrmKeyId> > cached_drm_key_ids_;
+ bool hdcp_lost_;
+};
+
+} // namespace shared
+} // namespace android
+} // namespace starboard
+
+#endif // STARBOARD_ANDROID_SHARED_DRM_SYSTEM_H_
diff --git a/src/starboard/android/shared/egl_swap_buffers.cc b/src/starboard/android/shared/egl_swap_buffers.cc
new file mode 100644
index 0000000..e1567f8
--- /dev/null
+++ b/src/starboard/android/shared/egl_swap_buffers.cc
@@ -0,0 +1,44 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <EGL/egl.h>
+#include <GLES2/gl2.h>
+
+#include "starboard/android/shared/video_window.h"
+#include "starboard/shared/gles/gl_call.h"
+
+extern "C" {
+EGLBoolean __real_eglSwapBuffers(EGLDisplay dpy, EGLSurface surface);
+
+// This needs to be exported to ensure shared_library targets include it.
+SB_EXPORT_PLATFORM EGLBoolean __wrap_eglSwapBuffers(
+ EGLDisplay dpy, EGLSurface surface) {
+ // Kick off the GPU while waiting for new player bounds to take effect.
+ GL_CALL(glFlush());
+
+ // Wait for player bounds to take effect before presenting the UI frame which
+ // uses those player bounds. This helps to keep punch-out videos in sync with
+ // the UI frame.
+ // TODO: It is possible for the new UI frame to be displayed too late
+ // (especially if there's a lot to render), so this case still needs to
+ // be handled.
+
+ // Note, we're no longer calling WaitForVideoBoundsUpdate because it does
+ // not work properly without calling SurfaceHolder setFixedSize.
+ // starboard::android::shared::WaitForVideoBoundsUpdate();
+
+ return __real_eglSwapBuffers(dpy, surface);
+}
+
+}
diff --git a/src/starboard/android/shared/file_can_open.cc b/src/starboard/android/shared/file_can_open.cc
new file mode 100644
index 0000000..5aeb5fd
--- /dev/null
+++ b/src/starboard/android/shared/file_can_open.cc
@@ -0,0 +1,43 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/file.h"
+
+#include <android/asset_manager.h>
+
+#include "starboard/directory.h"
+
+#include "starboard/android/shared/file_internal.h"
+#include "starboard/shared/posix/impl/file_can_open.h"
+
+using starboard::android::shared::IsAndroidAssetPath;
+using starboard::android::shared::OpenAndroidAsset;
+
+bool SbFileCanOpen(const char* path, int flags) {
+ if (!IsAndroidAssetPath(path)) {
+ return ::starboard::shared::posix::impl::FileCanOpen(path, flags);
+ }
+
+ SbFile file = SbFileOpen(path, flags | kSbFileOpenOnly, NULL, NULL);
+ bool result = SbFileIsValid(file);
+ SbFileClose(file);
+
+ if (!result) {
+ SbDirectory directory = SbDirectoryOpen(path, NULL);
+ result = SbDirectoryIsValid(directory);
+ SbDirectoryClose(directory);
+ }
+
+ return result;
+}
diff --git a/src/starboard/android/shared/file_close.cc b/src/starboard/android/shared/file_close.cc
new file mode 100644
index 0000000..192aede
--- /dev/null
+++ b/src/starboard/android/shared/file_close.cc
@@ -0,0 +1,30 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/file.h"
+
+#include <android/asset_manager.h>
+
+#include "starboard/android/shared/file_internal.h"
+#include "starboard/shared/posix/impl/file_close.h"
+
+bool SbFileClose(SbFile file) {
+ if (file && file->asset) {
+ AAsset_close(file->asset);
+ delete file;
+ return true;
+ }
+
+ return ::starboard::shared::posix::impl::FileClose(file);
+}
diff --git a/src/starboard/android/shared/file_delete.cc b/src/starboard/android/shared/file_delete.cc
new file mode 100644
index 0000000..9a0ab8b
--- /dev/null
+++ b/src/starboard/android/shared/file_delete.cc
@@ -0,0 +1,22 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/file.h"
+
+#include "starboard/android/shared/file_internal.h"
+#include "starboard/shared/posix/impl/file_delete.h"
+
+bool SbFileDelete(const char* path) {
+ return ::starboard::shared::posix::impl::FileDelete(path);
+}
diff --git a/src/starboard/android/shared/file_exists.cc b/src/starboard/android/shared/file_exists.cc
new file mode 100644
index 0000000..cfe253c
--- /dev/null
+++ b/src/starboard/android/shared/file_exists.cc
@@ -0,0 +1,19 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/file.h"
+
+bool SbFileExists(const char* path) {
+ return SbFileCanOpen(path, kSbFileRead);
+}
diff --git a/src/starboard/android/shared/file_flush.cc b/src/starboard/android/shared/file_flush.cc
new file mode 100644
index 0000000..25634ca
--- /dev/null
+++ b/src/starboard/android/shared/file_flush.cc
@@ -0,0 +1,22 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/file.h"
+
+#include "starboard/android/shared/file_internal.h"
+#include "starboard/shared/posix/impl/file_flush.h"
+
+bool SbFileFlush(SbFile file) {
+ return ::starboard::shared::posix::impl::FileFlush(file);
+}
diff --git a/src/starboard/android/shared/file_get_info.cc b/src/starboard/android/shared/file_get_info.cc
new file mode 100644
index 0000000..d7e7559
--- /dev/null
+++ b/src/starboard/android/shared/file_get_info.cc
@@ -0,0 +1,34 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/file.h"
+
+#include <android/asset_manager.h>
+
+#include "starboard/android/shared/file_internal.h"
+#include "starboard/shared/posix/impl/file_get_info.h"
+
+bool SbFileGetInfo(SbFile file, SbFileInfo* out_info) {
+ if (file && file->asset && out_info) {
+ out_info->creation_time = 0;
+ out_info->is_directory = 0;
+ out_info->is_symbolic_link = 0;
+ out_info->last_accessed = 0;
+ out_info->last_modified = 0;
+ out_info->size = AAsset_getLength(file->asset);
+ return true;
+ }
+
+ return ::starboard::shared::posix::impl::FileGetInfo(file, out_info);
+}
diff --git a/src/starboard/android/shared/file_get_path_info.cc b/src/starboard/android/shared/file_get_path_info.cc
new file mode 100644
index 0000000..9bc05dc
--- /dev/null
+++ b/src/starboard/android/shared/file_get_path_info.cc
@@ -0,0 +1,51 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/file.h"
+
+#include "starboard/directory.h"
+
+#include "starboard/android/shared/directory_internal.h"
+#include "starboard/android/shared/file_internal.h"
+#include "starboard/shared/posix/impl/file_get_path_info.h"
+
+using starboard::android::shared::IsAndroidAssetPath;
+using starboard::android::shared::OpenAndroidAsset;
+
+bool SbFileGetPathInfo(const char* path, SbFileInfo* out_info) {
+ if (!IsAndroidAssetPath(path)) {
+ return ::starboard::shared::posix::impl::FileGetPathInfo(path, out_info);
+ }
+
+ SbFile file = SbFileOpen(path, kSbFileRead, NULL, NULL);
+ if (file) {
+ bool result = SbFileGetInfo(file, out_info);
+ SbFileClose(file);
+ return result;
+ }
+
+ SbDirectory directory = SbDirectoryOpen(path, NULL);
+ if (directory && directory->asset_dir) {
+ out_info->creation_time = 0;
+ out_info->is_directory = 1;
+ out_info->is_symbolic_link = 0;
+ out_info->last_accessed = 0;
+ out_info->last_modified = 0;
+ out_info->size = 0;
+ SbDirectoryClose(directory);
+ return true;
+ }
+
+ return false;
+}
diff --git a/src/starboard/android/shared/file_internal.cc b/src/starboard/android/shared/file_internal.cc
new file mode 100644
index 0000000..fd7ae03
--- /dev/null
+++ b/src/starboard/android/shared/file_internal.cc
@@ -0,0 +1,138 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/file_internal.h"
+
+#include <android/asset_manager.h>
+#include <android/asset_manager_jni.h>
+#include <android/log.h>
+#include <jni.h>
+#include <string>
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/log.h"
+#include "starboard/memory.h"
+#include "starboard/string.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+const char* g_app_assets_dir = "/cobalt/assets";
+const char* g_app_files_dir = NULL;
+const char* g_app_cache_dir = NULL;
+const char* g_app_lib_dir = NULL;
+
+namespace {
+jobject g_java_asset_manager;
+AAssetManager* g_asset_manager;
+
+// Copies the characters from a jstring and returns a newly allocated buffer
+// with the result.
+const char* DuplicateJavaString(JniEnvExt* env, jstring j_string) {
+ SB_DCHECK(j_string);
+ std::string utf_str = env->GetStringStandardUTFOrAbort(j_string);
+ const char* result = SbStringDuplicate(utf_str.c_str());
+ return result;
+}
+
+} // namespace
+
+void SbFileAndroidInitialize() {
+ JniEnvExt* env = JniEnvExt::Get();
+
+ SB_DCHECK(g_java_asset_manager == NULL);
+ SB_DCHECK(g_asset_manager == NULL);
+ ScopedLocalJavaRef<jstring> j_app(
+ env->CallStarboardObjectMethodOrAbort(
+ "getApplicationContext", "()Landroid/content/Context;"));
+ g_java_asset_manager = env->ConvertLocalRefToGlobalRef(
+ env->CallObjectMethodOrAbort(j_app.Get(),
+ "getAssets", "()Landroid/content/res/AssetManager;"));
+ g_asset_manager = AAssetManager_fromJava(env, g_java_asset_manager);
+
+ SB_DCHECK(g_app_files_dir == NULL);
+ ScopedLocalJavaRef<jstring> j_string(
+ env->CallStarboardObjectMethodOrAbort("getFilesAbsolutePath",
+ "()Ljava/lang/String;"));
+ g_app_files_dir = DuplicateJavaString(env, j_string.Get());
+ SB_DLOG(INFO) << "Files dir: " << g_app_files_dir;
+
+ SB_DCHECK(g_app_cache_dir == NULL);
+ j_string.Reset(
+ env->CallStarboardObjectMethodOrAbort("getCacheAbsolutePath",
+ "()Ljava/lang/String;"));
+ g_app_cache_dir = DuplicateJavaString(env, j_string.Get());
+ SB_DLOG(INFO) << "Cache dir: " << g_app_cache_dir;
+
+ SB_DCHECK(g_app_lib_dir == NULL);
+ ScopedLocalJavaRef<jobject> j_app_info(
+ env->CallObjectMethodOrAbort(j_app.Get(), "getApplicationInfo",
+ "()Landroid/content/pm/ApplicationInfo;"));
+ j_string.Reset(env->GetStringFieldOrAbort(j_app_info.Get(),
+ "nativeLibraryDir"));
+ g_app_lib_dir = DuplicateJavaString(env, j_string.Get());
+ SB_DLOG(INFO) << "Lib dir: " << g_app_lib_dir;
+}
+
+void SbFileAndroidTeardown() {
+ JniEnvExt* env = JniEnvExt::Get();
+
+ if (g_java_asset_manager) {
+ env->DeleteGlobalRef(g_java_asset_manager);
+ g_java_asset_manager = NULL;
+ g_asset_manager = NULL;
+ }
+
+ if (g_app_files_dir) {
+ SbMemoryDeallocate(const_cast<char*>(g_app_files_dir));
+ g_app_files_dir = NULL;
+ }
+
+ if (g_app_cache_dir) {
+ SbMemoryDeallocate(const_cast<char*>(g_app_cache_dir));
+ g_app_cache_dir = NULL;
+ }
+}
+
+bool IsAndroidAssetPath(const char* path) {
+ size_t prefix_len = SbStringGetLength(g_app_assets_dir);
+ return path != NULL
+ && SbStringCompare(g_app_assets_dir, path, prefix_len) == 0
+ && (path[prefix_len] == '/' || path[prefix_len] == '\0');
+}
+
+AAsset* OpenAndroidAsset(const char* path) {
+ if (!IsAndroidAssetPath(path) || g_asset_manager == NULL) {
+ return NULL;
+ }
+ const char* asset_path = path + SbStringGetLength(g_app_assets_dir) + 1;
+ return AAssetManager_open(g_asset_manager, asset_path, AASSET_MODE_RANDOM);
+}
+
+AAssetDir* OpenAndroidAssetDir(const char* path) {
+ if (!IsAndroidAssetPath(path) || g_asset_manager == NULL) {
+ return NULL;
+ }
+ const char* asset_path = path + SbStringGetLength(g_app_assets_dir);
+ if (*asset_path == '/') {
+ asset_path++;
+ }
+ return AAssetManager_openDir(g_asset_manager, asset_path);
+}
+
+} // namespace shared
+} // namespace android
+} // namespace starboard
diff --git a/src/starboard/android/shared/file_internal.h b/src/starboard/android/shared/file_internal.h
new file mode 100644
index 0000000..7d33227
--- /dev/null
+++ b/src/starboard/android/shared/file_internal.h
@@ -0,0 +1,57 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_FILE_INTERNAL_H_
+#define STARBOARD_ANDROID_SHARED_FILE_INTERNAL_H_
+
+#include <errno.h>
+
+#include <android/asset_manager.h>
+
+#include "starboard/file.h"
+#include "starboard/shared/internal_only.h"
+
+struct SbFilePrivate {
+ // Note: Only one of these two fields will be valid for any given file.
+
+ // The POSIX file descriptor of this file, or -1 if it's a read-only asset.
+ int descriptor;
+
+ // If not NULL this is an Android asset.
+ AAsset* asset;
+
+ SbFilePrivate() : descriptor(-1), asset(NULL) {}
+};
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+extern const char* g_app_assets_dir;
+extern const char* g_app_files_dir;
+extern const char* g_app_cache_dir;
+extern const char* g_app_lib_dir;
+
+void SbFileAndroidInitialize();
+void SbFileAndroidTeardown();
+
+bool IsAndroidAssetPath(const char* path);
+AAsset* OpenAndroidAsset(const char* path);
+AAssetDir* OpenAndroidAssetDir(const char* path);
+
+} // namespace shared
+} // namespace android
+} // namespace starboard
+
+#endif // STARBOARD_ANDROID_SHARED_FILE_INTERNAL_H_
diff --git a/src/starboard/android/shared/file_open.cc b/src/starboard/android/shared/file_open.cc
new file mode 100644
index 0000000..83d924a
--- /dev/null
+++ b/src/starboard/android/shared/file_open.cc
@@ -0,0 +1,106 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/file.h"
+
+#include <android/asset_manager.h>
+
+#include <string>
+
+#include "starboard/android/shared/file_internal.h"
+#include "starboard/shared/posix/impl/file_open.h"
+
+using starboard::android::shared::IsAndroidAssetPath;
+using starboard::android::shared::OpenAndroidAsset;
+
+namespace {
+
+// We don't package most font files in Cobalt content and fallback to the system
+// font file of the same name.
+const std::string kFontsXml("fonts.xml");
+const std::string kSystemFontsDir("/system/fonts/");
+const std::string kCobaltFontsDir("/cobalt/assets/fonts/");
+
+// Returns the fallback for the given asset path, or an empty string if none.
+// NOTE: While Cobalt now provides a mechanism for loading system fonts through
+// SbSystemGetPath(), using the fallback logic within SbFileOpen() is
+// still preferred for Android's fonts. The reason for this is that the
+// Android OS actually allows fonts to be loaded from two locations: one
+// that it provides; and one that the devices running its OS, which it
+// calls vendors, can provide. Rather than including the full Android font
+// package, vendors have the option of using a smaller Android font
+// package and supplementing it with their own fonts.
+//
+// If Android were to use SbSystemGetPath() for its fonts, vendors would
+// have no way of providing those supplemental fonts to Cobalt, which
+// could result in a limited selection of fonts being available. By
+// treating Android's fonts as Cobalt's fonts, Cobalt can still offer a
+// straightforward mechanism for including vendor fonts via
+// SbSystemGetPath().
+std::string FallbackPath(const std::string& path) {
+ // Fonts fallback to the system fonts.
+ if (path.compare(0, kCobaltFontsDir.length(), kCobaltFontsDir) == 0) {
+ std::string file_name = path.substr(kCobaltFontsDir.length());
+ // fonts.xml doesn't fallback.
+ if (file_name != kFontsXml) {
+ return kSystemFontsDir + file_name;
+ }
+ }
+ return std::string();
+}
+
+} // namespace
+
+SbFile SbFileOpen(const char* path,
+ int flags,
+ bool* out_created,
+ SbFileError* out_error) {
+ if (!IsAndroidAssetPath(path)) {
+ return ::starboard::shared::posix::impl::FileOpen(
+ path, flags, out_created, out_error);
+ }
+
+ // Assets are never created and are always read-only, whether it's actually an
+ // asset or we end up opening a fallback path.
+ if (out_created) {
+ *out_created = false;
+ }
+ bool can_read = flags & kSbFileRead;
+ bool can_write = flags & kSbFileWrite;
+ if (!can_read || can_write) {
+ if (out_error) {
+ *out_error = kSbFileErrorAccessDenied;
+ }
+ return kSbFileInvalid;
+ }
+
+ AAsset* asset = OpenAndroidAsset(path);
+ if (asset) {
+ SbFile result = new SbFilePrivate();
+ result->asset = asset;
+ return result;
+ }
+
+ std::string fallback_path = FallbackPath(path);
+ if (!fallback_path.empty()) {
+ SbFile result = ::starboard::shared::posix::impl::FileOpen(
+ fallback_path.c_str(), flags, out_created, out_error);
+ return result;
+ }
+
+ if (out_error) {
+ *out_error = kSbFileErrorFailed;
+ }
+ return kSbFileInvalid;
+}
diff --git a/src/starboard/android/shared/file_read.cc b/src/starboard/android/shared/file_read.cc
new file mode 100644
index 0000000..d283019
--- /dev/null
+++ b/src/starboard/android/shared/file_read.cc
@@ -0,0 +1,32 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/file.h"
+
+#include <android/asset_manager.h>
+
+#include "starboard/android/shared/file_internal.h"
+#include "starboard/shared/posix/impl/file_read.h"
+
+int SbFileRead(SbFile file, char* data, int size) {
+ if (!file || size < 0) {
+ return -1;
+ }
+
+ if (file->asset) {
+ return AAsset_read(file->asset, data, size);
+ } else {
+ return ::starboard::shared::posix::impl::FileRead(file, data, size);
+ }
+}
diff --git a/src/starboard/android/shared/file_seek.cc b/src/starboard/android/shared/file_seek.cc
new file mode 100644
index 0000000..590446b
--- /dev/null
+++ b/src/starboard/android/shared/file_seek.cc
@@ -0,0 +1,28 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/file.h"
+
+#include <android/asset_manager.h>
+
+#include "starboard/android/shared/file_internal.h"
+#include "starboard/shared/posix/impl/file_seek.h"
+
+int64_t SbFileSeek(SbFile file, SbFileWhence whence, int64_t offset) {
+ if (file && file->asset) {
+ return AAsset_seek64(file->asset, offset, whence);
+ } else {
+ return ::starboard::shared::posix::impl::FileSeek(file, whence, offset);
+ }
+}
diff --git a/src/starboard/android/shared/file_truncate.cc b/src/starboard/android/shared/file_truncate.cc
new file mode 100644
index 0000000..c1075e7
--- /dev/null
+++ b/src/starboard/android/shared/file_truncate.cc
@@ -0,0 +1,22 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/file.h"
+
+#include "starboard/android/shared/file_internal.h"
+#include "starboard/shared/posix/impl/file_truncate.h"
+
+bool SbFileTruncate(SbFile file, int64_t length) {
+ return ::starboard::shared::posix::impl::FileTruncate(file, length);
+}
diff --git a/src/starboard/android/shared/file_write.cc b/src/starboard/android/shared/file_write.cc
new file mode 100644
index 0000000..7705435
--- /dev/null
+++ b/src/starboard/android/shared/file_write.cc
@@ -0,0 +1,22 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/file.h"
+
+#include "starboard/android/shared/file_internal.h"
+#include "starboard/shared/posix/impl/file_write.h"
+
+int SbFileWrite(SbFile file, const char* data, int size) {
+ return ::starboard::shared::posix::impl::FileWrite(file, data, size);
+}
diff --git a/src/starboard/android/shared/get_home_directory.cc b/src/starboard/android/shared/get_home_directory.cc
new file mode 100644
index 0000000..0d75716
--- /dev/null
+++ b/src/starboard/android/shared/get_home_directory.cc
@@ -0,0 +1,43 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <pwd.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "starboard/android/shared/file_internal.h"
+#include "starboard/log.h"
+#include "starboard/shared/nouser/user_internal.h"
+#include "starboard/string.h"
+
+using ::starboard::android::shared::g_app_files_dir;
+
+namespace starboard {
+namespace shared {
+namespace nouser {
+
+// With a single SbUser representing the Android platform, the 'file_storage'
+// SbStorage implementation writes a single SbStorageRecord for all accounts in
+// the home directory we return here. This is analogous to a web browser
+// providing a single cookie store no matter what any particular web app does to
+// present signing in as different users.
+bool GetHomeDirectory(SbUser user, char* out_path, int path_size) {
+ int len = SbStringCopy(out_path, g_app_files_dir, path_size);
+ return len < path_size;
+}
+
+} // namespace nouser
+} // namespace shared
+} // namespace starboard
diff --git a/src/starboard/android/shared/gyp_configuration.gypi b/src/starboard/android/shared/gyp_configuration.gypi
new file mode 100644
index 0000000..10f6c0c
--- /dev/null
+++ b/src/starboard/android/shared/gyp_configuration.gypi
@@ -0,0 +1,133 @@
+# Copyright 2016 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Platform specific configuration for Android on Starboard. Automatically
+# included by gyp_cobalt in all .gyp files by Cobalt together with base.gypi.
+#
+{
+ 'variables': {
+ 'target_os': 'android',
+ 'final_executable_type': 'shared_library',
+ 'gtest_target_type': 'shared_library',
+
+ 'gl_type': 'system_gles2',
+ 'enable_remote_debugging': 0,
+
+ # Define platform specific compiler and linker flags.
+ # Refer to base.gypi for a list of all available variables.
+ 'compiler_flags_host': [
+ '-O2',
+ ],
+ 'compiler_flags_debug': [
+ '-frtti',
+ '-O0',
+ ],
+ 'compiler_flags_devel': [
+ '-frtti',
+ '-O2',
+ ],
+ 'compiler_flags_qa': [
+ '-fno-rtti',
+ '-O2',
+ '-gline-tables-only',
+ ],
+ 'compiler_flags_gold': [
+ '-fno-rtti',
+ '-O2',
+ '-gline-tables-only',
+ ],
+ 'platform_libraries': [
+ '-lEGL',
+ '-lGLESv2',
+ '-lOpenSLES',
+ '-landroid',
+ '-llog',
+ '-lmediandk',
+ ],
+ 'conditions': [
+ ['cobalt_fastbuild==0', {
+ 'compiler_flags_debug': [
+ '-g',
+ ],
+ 'compiler_flags_devel': [
+ '-g',
+ ],
+ 'compiler_flags_qa': [
+ '-gline-tables-only',
+ ],
+ 'compiler_flags_gold': [
+ '-gline-tables-only',
+ ],
+ }],
+ ],
+ },
+
+ 'target_defaults': {
+ 'target_conditions': [
+ ['sb_pedantic_warnings==1', {
+ 'cflags': [
+ '-Wall',
+ '-Wextra',
+ '-Wunreachable-code',
+ # Don't get pedantic about warnings from base macros. These must be
+ # disabled after the -Wall above, so this has to be done here rather
+ # than in the platform's target toolchain.
+ # TODO: Rebase base and use static_assert instead of COMPILE_ASSERT
+ '-Wno-unused-local-typedef', # COMPILE_ASSERT
+ '-Wno-missing-field-initializers', # LAZY_INSTANCE_INITIALIZER
+ ],
+ }],
+ ['_type=="executable"', {
+ # Android Lollipop+ requires relocatable executables.
+ 'cflags': [
+ '-fPIE',
+ ],
+ 'ldflags': [
+ '-pie',
+ ],
+ },{
+ # Android requires relocatable shared libraries.
+ 'cflags': [
+ '-fPIC',
+ ],
+ }],
+ ['use_asan==1', {
+ 'cflags': [
+ '-fsanitize=address',
+ '-fno-omit-frame-pointer',
+ ],
+ 'ldflags': [
+ '-fsanitize=address',
+ # Force linking of the helpers in sanitizer_options.cc
+ '-Wl,-u_sanitizer_options_link_helper',
+ ],
+ 'defines': [
+ 'ADDRESS_SANITIZER',
+ ],
+ }],
+ ['use_tsan==1', {
+ 'cflags': [
+ '-fsanitize=thread',
+ '-fno-omit-frame-pointer',
+ ],
+ 'ldflags': [
+ '-fsanitize=thread',
+ ],
+ 'defines': [
+ 'THREAD_SANITIZER',
+ ],
+ }],
+ ],
+ }, # end of target_defaults
+}
diff --git a/src/starboard/android/shared/gyp_configuration.py b/src/starboard/android/shared/gyp_configuration.py
new file mode 100644
index 0000000..5c78367
--- /dev/null
+++ b/src/starboard/android/shared/gyp_configuration.py
@@ -0,0 +1,302 @@
+# Copyright 2016 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Starboard Android shared platform configuration for gyp_cobalt."""
+
+from __future__ import print_function
+
+import imp
+import os
+from subprocess import call
+
+import gyp_utils
+import starboard.android.shared.sdk_utils as sdk_utils
+from starboard.build.platform_configuration import PlatformConfiguration
+from starboard.tools.testing import test_filter
+from starboard.tools.toolchain import ar
+from starboard.tools.toolchain import bash
+from starboard.tools.toolchain import clang
+from starboard.tools.toolchain import clangxx
+from starboard.tools.toolchain import cp
+from starboard.tools.toolchain import touch
+
+_APK_DIR = os.path.join(os.path.dirname(__file__), os.path.pardir, 'apk')
+_APK_BUILD_ID_FILE = os.path.join(_APK_DIR, 'build.id')
+_COBALT_GRADLE = os.path.join(_APK_DIR, 'cobalt-gradle.sh')
+
+# Maps the Android ABI to the name of the toolchain.
+_ABI_TOOLCHAIN_NAME = {
+ 'x86': 'i686-linux-android',
+ 'armeabi': 'arm-linux-androideabi',
+ 'armeabi-v7a': 'arm-linux-androideabi',
+ 'arm64-v8a': 'aarch64-linux-android',
+}
+
+
+class AndroidConfiguration(PlatformConfiguration):
+ """Starboard Android platform configuration."""
+
+ # TODO: make ASAN work with NDK tools and enable it by default
+ def __init__(self, platform, android_abi, asan_enabled_by_default=False):
+ super(AndroidConfiguration, self).__init__(platform,
+ asan_enabled_by_default)
+ self._target_toolchain = None
+ self._host_toolchain = None
+
+ self.AppendApplicationConfigurationPath(os.path.dirname(__file__))
+
+ self.android_abi = android_abi
+
+ self.host_compiler_environment = gyp_utils.GetHostCompilerEnvironment()
+ self.android_home = sdk_utils.GetSdkPath()
+ self.android_ndk_home = sdk_utils.GetNdkPath()
+
+ print('Using Android SDK at {}'.format(self.android_home))
+ print('Using Android NDK at {}'.format(self.android_ndk_home))
+
+ def GetBuildFormat(self):
+ """Returns the desired build format."""
+ # The comma means that ninja and qtcreator_ninja will be chained and use the
+ # same input information so that .gyp files will only have to be parsed
+ # once.
+ return 'ninja,qtcreator_ninja'
+
+ def GetVariables(self, configuration):
+ variables = super(AndroidConfiguration, self).GetVariables(
+ configuration, use_clang=1)
+ variables.update({
+ 'ANDROID_HOME':
+ self.android_home,
+ 'NDK_HOME':
+ self.android_ndk_home,
+ 'ANDROID_ABI':
+ self.android_abi,
+ 'include_path_platform_deploy_gypi':
+ 'starboard/android/shared/platform_deploy.gypi',
+ 'javascript_engine':
+ 'v8',
+ 'cobalt_enable_jit':
+ 1,
+ })
+ return variables
+
+ def GetGeneratorVariables(self, configuration):
+ _ = configuration
+ generator_variables = {
+ 'qtcreator_session_name_prefix': 'cobalt',
+ }
+ return generator_variables
+
+ def GetEnvironmentVariables(self):
+ sdk_utils.InstallSdkIfNeeded(self.android_abi)
+ call([_COBALT_GRADLE, '--reset'])
+ with open(_APK_BUILD_ID_FILE, 'w') as build_id_file:
+ build_id_file.write('{}'.format(gyp_utils.GetBuildNumber()))
+
+ env_variables = {}
+
+ # Android builds tend to consume significantly more memory than the
+ # default settings permit, so cap this at 1 in order to avoid build
+ # issues. Without this, 32GB machines end up getting automatically
+ # configured to run 5 at a time, which can be too much for at least
+ # android-arm64_debug.
+ # TODO: Eventually replace this with something more robust, like an
+ # implementation of the abstract toolchain for Android.
+ env_variables.update({'GYP_LINK_CONCURRENCY': '1'})
+
+ return env_variables
+
+ def GetTargetToolchain(self):
+ if not self._target_toolchain:
+ tool_prefix = os.path.join(
+ sdk_utils.GetToolsPath(self.android_abi), 'bin',
+ _ABI_TOOLCHAIN_NAME[self.android_abi] + '-')
+ cc_path = tool_prefix + 'clang'
+ cxx_path = tool_prefix + 'clang++'
+ ar_path = tool_prefix + 'ar'
+ clang_flags = [
+ # We'll pretend not to be Linux, but Starboard instead.
+ '-U__linux__',
+
+ # libwebp uses the cpufeatures library to detect ARM NEON support
+ '-I{}/sources/android/cpufeatures'.format(self.android_ndk_home),
+
+ # Mimic build/cmake/android.toolchain.cmake in the Android NDK.
+ '-ffunction-sections',
+ '-funwind-tables',
+ '-fstack-protector-strong',
+ '-no-canonical-prefixes',
+
+ # Other flags
+ '-fsigned-char',
+ '-fno-limit-debug-info',
+ '-fno-exceptions',
+ '-fcolor-diagnostics',
+ '-fno-strict-aliasing', # See http://crbug.com/32204
+
+ # Default visibility is hidden to enable dead stripping.
+ '-fvisibility=hidden',
+ # Any warning will stop the build.
+ '-Werror',
+
+ # Disable errors for the warning till the Android NDK r19 is fixed.
+ # The warning is trigger when compiling .c files and complains for
+ # '-stdlib=libc++' which is added by the NDK.
+ '-Wno-error=unused-command-line-argument',
+
+ # Don't warn about register variables (in base and net)
+ '-Wno-deprecated-register',
+ # Don't warn about deprecated ICU methods (in googleurl and net)
+ '-Wno-deprecated-declarations',
+ # Skia doesn't use overrides.
+ '-Wno-inconsistent-missing-override',
+ # shifting a negative signed value is undefined
+ '-Wno-shift-negative-value',
+ # Don't warn for implicit sign conversions. (in v8 and protobuf)
+ '-Wno-sign-conversion',
+ ]
+ clang_defines = [
+ # Enable compile-time decisions based on the ABI
+ 'ANDROID_ABI={}'.format(self.android_abi),
+ # -DANDROID is an argument to some ifdefs in the NDK's eglplatform.h
+ 'ANDROID',
+ # Cobalt on Linux flag
+ 'COBALT_LINUX',
+ # So that we get the PRI* macros from inttypes.h
+ '__STDC_FORMAT_MACROS',
+ # Enable GNU extensions to get prototypes like ffsl.
+ '_GNU_SOURCE=1',
+ # Undefining __linux__ causes the system headers to make wrong
+ # assumptions about which C-library is used on the platform.
+ '__BIONIC__',
+ # Undefining __linux__ leaves libc++ without a threads implementation.
+ # TODO: See if there's a way to make libcpp threading use Starboard.
+ '_LIBCPP_HAS_THREAD_API_PTHREAD',
+ ]
+ linker_flags = [
+ # Use the static LLVM libc++.
+ '-static-libstdc++',
+
+ # Mimic build/cmake/android.toolchain.cmake in the Android NDK.
+ '-Wl,--build-id',
+ '-Wl,--warn-shared-textrel',
+ '-Wl,--fatal-warnings',
+ '-Wl,--gc-sections',
+ '-Wl,-z,nocopyreloc',
+
+ # Wrapper synchronizes punch-out video bounds with the UI frame.
+ '-Wl,--wrap=eglSwapBuffers',
+ ]
+ self._target_toolchain = [
+ clang.CCompiler(
+ path=cc_path, defines=clang_defines, extra_flags=clang_flags),
+ clang.CxxCompiler(
+ path=cxx_path,
+ defines=clang_defines,
+ extra_flags=clang_flags + [
+ '-std=c++11',
+ ]),
+ clang.AssemblerWithCPreprocessor(
+ path=cc_path, defines=clang_defines, extra_flags=clang_flags),
+ ar.StaticThinLinker(path=ar_path),
+ ar.StaticLinker(path=ar_path),
+ clangxx.SharedLibraryLinker(path=cxx_path, extra_flags=linker_flags),
+ clangxx.ExecutableLinker(path=cxx_path, extra_flags=linker_flags),
+ ]
+ return self._target_toolchain
+
+ def GetHostToolchain(self):
+ if not self._host_toolchain:
+ cc_path = self.host_compiler_environment['CC_host'],
+ cxx_path = self.host_compiler_environment['CXX_host']
+ self._host_toolchain = [
+ clang.CCompiler(path=cc_path),
+ clang.CxxCompiler(path=cxx_path),
+ clang.AssemblerWithCPreprocessor(path=cc_path),
+ ar.StaticThinLinker(),
+ ar.StaticLinker(),
+ clangxx.ExecutableLinker(path=cxx_path),
+ clangxx.SharedLibraryLinker(path=cxx_path),
+ cp.Copy(),
+ touch.Stamp(),
+ bash.Shell(),
+ ]
+ return self._host_toolchain
+
+ def GetLauncher(self):
+ """Gets the module used to launch applications on this platform."""
+ module_path = os.path.abspath(
+ os.path.join(os.path.dirname(__file__), 'launcher.py'))
+ launcher_module = imp.load_source('launcher', module_path)
+ return launcher_module
+
+ def GetTestFilters(self):
+ filters = super(AndroidConfiguration, self).GetTestFilters()
+ for target, tests in self._FILTERED_TESTS.iteritems():
+ filters.extend(test_filter.TestFilter(target, test) for test in tests)
+ return filters
+
+ # A map of failing or crashing tests per target.
+ _FILTERED_TESTS = {
+ 'nplb': [
+ 'SbAudioSinkTest.AllFramesConsumed',
+ 'SbAudioSinkTest.SomeFramesConsumed',
+ 'SbAudioSinkTest.Underflow',
+ 'SbSocketAddressTypes/SbSocketGetInterfaceAddressTest'
+ '.SunnyDayDestination/0',
+ 'SbSocketAddressTypes/SbSocketGetInterfaceAddressTest'
+ '.SunnyDaySourceForDestination/0',
+ 'SbSocketAddressTypes/SbSocketGetInterfaceAddressTest'
+ '.SunnyDaySourceForDestination/1',
+ 'SbSocketAddressTypes/SbSocketGetInterfaceAddressTest'
+ '.SunnyDaySourceNotLoopback/0',
+ 'SbSocketBindTest.SunnyDayLocalInterface',
+ 'SbSocketGetLocalAddressTest.SunnyDayBoundSpecified',
+ 'SpeechRecognizerTest.StartIsCalledMultipleTimes',
+ 'SpeechRecognizerTest.StartRecognizerWith10MaxAlternatives',
+ 'SpeechRecognizerTest.StartRecognizerWithContinuousRecognition',
+ 'SpeechRecognizerTest.StartRecognizerWithInterimResults',
+ 'SpeechRecognizerTest.StartTestSunnyDay',
+ ],
+ 'player_filter_tests': [
+ 'AudioDecoderTests/AudioDecoderTest.EndOfStreamWithoutAnyInput/0',
+ 'AudioDecoderTests/AudioDecoderTest.ResetBeforeInput/0',
+ 'AudioDecoderTests/AudioDecoderTest.SingleInput/0',
+ 'VideoDecoderTests/VideoDecoderTest.DecodeFullGOP/0',
+ 'VideoDecoderTests/VideoDecoderTest.DecodeFullGOP/1',
+ 'VideoDecoderTests/VideoDecoderTest.EndOfStreamWithoutAnyInput/0',
+ 'VideoDecoderTests/VideoDecoderTest.EndOfStreamWithoutAnyInput/1',
+ 'VideoDecoderTests/VideoDecoderTest.EndOfStreamWithoutAnyInput/2',
+ 'VideoDecoderTests/VideoDecoderTest.EndOfStreamWithoutAnyInput/3',
+ 'VideoDecoderTests/VideoDecoderTest'
+ '.GetCurrentDecodeTargetBeforeWriteInputBuffer/0',
+ 'VideoDecoderTests/VideoDecoderTest'
+ '.GetCurrentDecodeTargetBeforeWriteInputBuffer/2',
+ 'VideoDecoderTests/VideoDecoderTest.HoldFramesUntilFull/0',
+ 'VideoDecoderTests/VideoDecoderTest.HoldFramesUntilFull/1',
+
+ # On some platforms, and for some decoders (such as AVC), Android
+ # returns MEDIA_CODEC_DEQUEUE_OUTPUT_AGAIN_LATER for the test's
+ # invalid input frame instead of signaling an error, which the test is
+ # looking for.
+ 'VideoDecoderTests/VideoDecoderTest.SingleInvalidInput/0',
+ 'VideoDecoderTests/VideoDecoderTest.SingleInvalidInput/1',
+
+ # Android currently does not support multi-video playback, which
+ # the following tests depend upon.
+ 'VideoDecoderTests/VideoDecoderTest.ThreeMoreDecoders/0',
+ 'VideoDecoderTests/VideoDecoderTest.ThreeMoreDecoders/1',
+ 'VideoDecoderTests/VideoDecoderTest.ThreeMoreDecoders/2',
+ 'VideoDecoderTests/VideoDecoderTest.ThreeMoreDecoders/3',
+ ],
+ }
diff --git a/src/starboard/android/shared/input_events_generator.cc b/src/starboard/android/shared/input_events_generator.cc
new file mode 100644
index 0000000..f307963
--- /dev/null
+++ b/src/starboard/android/shared/input_events_generator.cc
@@ -0,0 +1,970 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/input_events_generator.h"
+
+#include <android/input.h>
+#include <android/keycodes.h>
+#include <jni.h>
+#include <math.h>
+
+#include "starboard/android/shared/application_android.h"
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/double.h"
+#include "starboard/key.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+using ::starboard::shared::starboard::Application;
+typedef ::starboard::android::shared::InputEventsGenerator::Event Event;
+typedef ::starboard::android::shared::InputEventsGenerator::Events Events;
+
+namespace {
+
+SbKeyLocation AInputEventToSbKeyLocation(AInputEvent *event) {
+ int32_t keycode = AKeyEvent_getKeyCode(event);
+ switch (keycode) {
+ case AKEYCODE_ALT_LEFT:
+ case AKEYCODE_CTRL_LEFT:
+ case AKEYCODE_META_LEFT:
+ case AKEYCODE_SHIFT_LEFT:
+ return kSbKeyLocationLeft;
+ case AKEYCODE_ALT_RIGHT:
+ case AKEYCODE_CTRL_RIGHT:
+ case AKEYCODE_META_RIGHT:
+ case AKEYCODE_SHIFT_RIGHT:
+ return kSbKeyLocationRight;
+ }
+ return kSbKeyLocationUnspecified;
+}
+
+unsigned int AInputEventToSbModifiers(AInputEvent *event) {
+ int32_t meta = AKeyEvent_getMetaState(event);
+ unsigned int modifiers = kSbKeyModifiersNone;
+ if (meta & AMETA_ALT_ON) {
+ modifiers |= kSbKeyModifiersAlt;
+ }
+ if (meta & AMETA_CTRL_ON) {
+ modifiers |= kSbKeyModifiersCtrl;
+ }
+ if (meta & AMETA_META_ON) {
+ modifiers |= kSbKeyModifiersMeta;
+ }
+ if (meta & AMETA_SHIFT_ON) {
+ modifiers |= kSbKeyModifiersShift;
+ }
+ return modifiers;
+}
+
+std::unique_ptr<Event> CreateMoveEventWithKey(
+ int32_t device_id,
+ SbWindow window,
+ SbKey key,
+ SbKeyLocation location,
+ const SbInputVector& input_vector) {
+ std::unique_ptr<SbInputData> data(new SbInputData());
+ SbMemorySet(data.get(), 0, sizeof(*data));
+
+ // window
+ data->window = window;
+ data->type = kSbInputEventTypeMove;
+ data->device_type = kSbInputDeviceTypeGamepad;
+ data->device_id = device_id;
+
+ // key
+ data->key = key;
+ data->key_location = location;
+ data->key_modifiers = kSbKeyModifiersNone;
+ data->position = input_vector;
+
+ return std::unique_ptr<Event>(
+ new Application::Event(kSbEventTypeInput, data.release(),
+ &Application::DeleteDestructor<SbInputData>));
+}
+
+float GetFlat(jobject input_device, int axis) {
+ JniEnvExt* env = JniEnvExt::Get();
+ ScopedLocalJavaRef<jobject> motion_range(env->CallObjectMethodOrAbort(
+ input_device, "getMotionRange",
+ "(I)Landroid/view/InputDevice$MotionRange;", axis));
+
+ float flat = env->CallFloatMethodOrAbort(
+ motion_range.Get(), "getFlat", "()F");
+
+ SB_DCHECK(flat < 1.0f);
+ return flat;
+}
+
+bool IsDPadKey(SbKey key) {
+ return key == kSbKeyGamepadDPadUp || key == kSbKeyGamepadDPadDown ||
+ key == kSbKeyGamepadDPadLeft || key == kSbKeyGamepadDPadRight;
+}
+
+SbKey AInputEventToSbKey(AInputEvent* event) {
+ int32_t keycode = AKeyEvent_getKeyCode(event);
+ switch (keycode) {
+ // Modifiers
+ case AKEYCODE_ALT_LEFT:
+ case AKEYCODE_ALT_RIGHT:
+ case AKEYCODE_MENU:
+ return kSbKeyMenu;
+ case AKEYCODE_CTRL_LEFT:
+ case AKEYCODE_CTRL_RIGHT:
+ return kSbKeyControl;
+ case AKEYCODE_META_LEFT:
+ return kSbKeyLwin;
+ case AKEYCODE_META_RIGHT:
+ return kSbKeyRwin;
+ case AKEYCODE_SHIFT_LEFT:
+ case AKEYCODE_SHIFT_RIGHT:
+ return kSbKeyShift;
+ case AKEYCODE_CAPS_LOCK:
+ return kSbKeyCapital;
+ case AKEYCODE_NUM_LOCK:
+ return kSbKeyNumlock;
+ case AKEYCODE_SCROLL_LOCK:
+ return kSbKeyScroll;
+
+ // System functions
+ case AKEYCODE_SLEEP:
+ return kSbKeySleep;
+ case AKEYCODE_HELP:
+ return kSbKeyHelp;
+
+ // Navigation
+ case AKEYCODE_BACK: // Android back button, not backspace
+ case AKEYCODE_ESCAPE:
+ return kSbKeyEscape;
+
+ // Enter/Select
+ case AKEYCODE_ENTER:
+ case AKEYCODE_NUMPAD_ENTER:
+ return kSbKeyReturn;
+
+ // Focus movement
+ case AKEYCODE_PAGE_UP:
+ return kSbKeyPrior;
+ case AKEYCODE_PAGE_DOWN:
+ return kSbKeyNext;
+ case AKEYCODE_MOVE_HOME:
+ return kSbKeyHome;
+ case AKEYCODE_MOVE_END:
+ return kSbKeyEnd;
+
+ // D-pad
+ case AKEYCODE_DPAD_UP:
+ return kSbKeyGamepadDPadUp;
+ case AKEYCODE_DPAD_DOWN:
+ return kSbKeyGamepadDPadDown;
+ case AKEYCODE_DPAD_LEFT:
+ return kSbKeyGamepadDPadLeft;
+ case AKEYCODE_DPAD_RIGHT:
+ return kSbKeyGamepadDPadRight;
+ case AKEYCODE_DPAD_CENTER:
+ return kSbKeyGamepad1;
+
+ // Game controller
+ case AKEYCODE_BUTTON_A:
+ return kSbKeyGamepad1;
+ case AKEYCODE_BUTTON_B:
+ return kSbKeyGamepad2;
+ case AKEYCODE_BUTTON_C:
+ return kSbKeyUnknown;
+ case AKEYCODE_BUTTON_X:
+ return kSbKeyGamepad3;
+ case AKEYCODE_BUTTON_Y:
+ return kSbKeyGamepad4;
+ case AKEYCODE_BUTTON_L1:
+ return kSbKeyGamepadLeftBumper;
+ case AKEYCODE_BUTTON_R1:
+ return kSbKeyGamepadRightBumper;
+ case AKEYCODE_BUTTON_L2:
+ return kSbKeyGamepadLeftTrigger;
+ case AKEYCODE_BUTTON_R2:
+ return kSbKeyGamepadRightTrigger;
+ case AKEYCODE_BUTTON_THUMBL:
+ return kSbKeyGamepadLeftStick;
+ case AKEYCODE_BUTTON_THUMBR:
+ return kSbKeyGamepadRightStick;
+ case AKEYCODE_BUTTON_START:
+ return kSbKeyGamepad6;
+ case AKEYCODE_BUTTON_SELECT:
+ return kSbKeyGamepad5;
+ case AKEYCODE_BUTTON_MODE:
+ return kSbKeyModechange;
+
+ // Media transport
+ case AKEYCODE_MEDIA_PLAY_PAUSE:
+ return kSbKeyMediaPlayPause;
+ case AKEYCODE_MEDIA_PLAY:
+ return kSbKeyPlay;
+ case AKEYCODE_MEDIA_PAUSE:
+ return kSbKeyPause;
+ case AKEYCODE_MEDIA_STOP:
+ return kSbKeyMediaStop;
+ case AKEYCODE_MEDIA_NEXT:
+ return kSbKeyMediaNextTrack;
+ case AKEYCODE_MEDIA_PREVIOUS:
+ return kSbKeyMediaPrevTrack;
+ case AKEYCODE_MEDIA_REWIND:
+ return kSbKeyMediaRewind;
+ case AKEYCODE_MEDIA_FAST_FORWARD:
+ return kSbKeyMediaFastForward;
+
+#if SB_API_VERSION >= 6
+ // TV Remote specific
+ case AKEYCODE_CHANNEL_UP:
+ return kSbKeyChannelUp;
+ case AKEYCODE_CHANNEL_DOWN:
+ return kSbKeyChannelDown;
+ case AKEYCODE_CAPTIONS:
+ return kSbKeyClosedCaption;
+ case AKEYCODE_INFO:
+ return kSbKeyInfo;
+ case AKEYCODE_GUIDE:
+ return kSbKeyGuide;
+ case AKEYCODE_LAST_CHANNEL:
+ return kSbKeyLast;
+ case AKEYCODE_MEDIA_AUDIO_TRACK:
+ return kSbKeyMediaAudioTrack;
+
+ case AKEYCODE_PROG_RED:
+ return kSbKeyRed;
+ case AKEYCODE_PROG_GREEN:
+ return kSbKeyGreen;
+ case AKEYCODE_PROG_YELLOW:
+ return kSbKeyYellow;
+ case AKEYCODE_PROG_BLUE:
+ return kSbKeyBlue;
+#endif // SB_API_VERSION >= 6
+
+ // Whitespace
+ case AKEYCODE_TAB:
+ return kSbKeyTab;
+ case AKEYCODE_SPACE:
+ return kSbKeySpace;
+
+ // Deletion
+ case AKEYCODE_DEL: // Backspace
+ return kSbKeyBack;
+ case AKEYCODE_FORWARD_DEL:
+ return kSbKeyDelete;
+ case AKEYCODE_CLEAR:
+ return kSbKeyClear;
+
+ // Insert
+ case AKEYCODE_INSERT:
+ return kSbKeyInsert;
+
+ // Symbols
+ case AKEYCODE_NUMPAD_ADD:
+ return kSbKeyAdd;
+ case AKEYCODE_PLUS:
+ case AKEYCODE_EQUALS:
+ case AKEYCODE_NUMPAD_EQUALS:
+ return kSbKeyOemPlus;
+ case AKEYCODE_NUMPAD_SUBTRACT:
+ return kSbKeySubtract;
+ case AKEYCODE_MINUS:
+ return kSbKeyOemMinus;
+ case AKEYCODE_NUMPAD_MULTIPLY:
+ return kSbKeyMultiply;
+ case AKEYCODE_NUMPAD_DIVIDE:
+ return kSbKeyDivide;
+ case AKEYCODE_COMMA:
+ case AKEYCODE_NUMPAD_COMMA:
+ return kSbKeyOemComma;
+ case AKEYCODE_NUMPAD_DOT:
+ return kSbKeyDecimal;
+ case AKEYCODE_PERIOD:
+ return kSbKeyOemPeriod;
+ case AKEYCODE_SEMICOLON:
+ return kSbKeyOem1;
+ case AKEYCODE_SLASH:
+ return kSbKeyOem2;
+ case AKEYCODE_GRAVE:
+ return kSbKeyOem3;
+ case AKEYCODE_LEFT_BRACKET:
+ return kSbKeyOem4;
+ case AKEYCODE_BACKSLASH:
+ return kSbKeyOem5;
+ case AKEYCODE_RIGHT_BRACKET:
+ return kSbKeyOem6;
+ case AKEYCODE_APOSTROPHE:
+ return kSbKeyOem7;
+
+ // Function keys
+ case AKEYCODE_F1:
+ case AKEYCODE_F2:
+ case AKEYCODE_F3:
+ case AKEYCODE_F4:
+ case AKEYCODE_F5:
+ case AKEYCODE_F6:
+ case AKEYCODE_F7:
+ case AKEYCODE_F8:
+ case AKEYCODE_F9:
+ case AKEYCODE_F10:
+ case AKEYCODE_F11:
+ case AKEYCODE_F12:
+ return static_cast<SbKey>(kSbKeyF1 + (keycode - AKEYCODE_F1));
+
+ // Digits
+ case AKEYCODE_0:
+ case AKEYCODE_1:
+ case AKEYCODE_2:
+ case AKEYCODE_3:
+ case AKEYCODE_4:
+ case AKEYCODE_5:
+ case AKEYCODE_6:
+ case AKEYCODE_7:
+ case AKEYCODE_8:
+ case AKEYCODE_9:
+ return static_cast<SbKey>(kSbKey0 + (keycode - AKEYCODE_0));
+
+ // Numpad digits
+ case AKEYCODE_NUMPAD_0:
+ case AKEYCODE_NUMPAD_1:
+ case AKEYCODE_NUMPAD_2:
+ case AKEYCODE_NUMPAD_3:
+ case AKEYCODE_NUMPAD_4:
+ case AKEYCODE_NUMPAD_5:
+ case AKEYCODE_NUMPAD_6:
+ case AKEYCODE_NUMPAD_7:
+ case AKEYCODE_NUMPAD_8:
+ case AKEYCODE_NUMPAD_9:
+ return static_cast<SbKey>(kSbKeyNumpad0 + (keycode - AKEYCODE_NUMPAD_0));
+
+ // Alphabetic
+ case AKEYCODE_A:
+ case AKEYCODE_B:
+ case AKEYCODE_C:
+ case AKEYCODE_D:
+ case AKEYCODE_E:
+ case AKEYCODE_F:
+ case AKEYCODE_G:
+ case AKEYCODE_H:
+ case AKEYCODE_I:
+ case AKEYCODE_J:
+ case AKEYCODE_K:
+ case AKEYCODE_L:
+ case AKEYCODE_M:
+ case AKEYCODE_N:
+ case AKEYCODE_O:
+ case AKEYCODE_P:
+ case AKEYCODE_Q:
+ case AKEYCODE_R:
+ case AKEYCODE_S:
+ case AKEYCODE_T:
+ case AKEYCODE_U:
+ case AKEYCODE_V:
+ case AKEYCODE_W:
+ case AKEYCODE_X:
+ case AKEYCODE_Y:
+ case AKEYCODE_Z:
+ return static_cast<SbKey>(kSbKeyA + (keycode - AKEYCODE_A));
+
+ // Don't handle these keys so the OS can in a uniform manner.
+ case AKEYCODE_VOLUME_UP:
+ case AKEYCODE_VOLUME_DOWN:
+ case AKEYCODE_MUTE:
+ case AKEYCODE_BRIGHTNESS_UP:
+ case AKEYCODE_BRIGHTNESS_DOWN:
+ case AKEYCODE_SEARCH:
+ default:
+ return kSbKeyUnknown;
+ }
+}
+
+} // namespace
+
+InputEventsGenerator::InputEventsGenerator(SbWindow window)
+ : window_(window),
+ hat_value_(),
+ left_thumbstick_key_pressed_{kSbKeyUnknown, kSbKeyUnknown} {
+ SB_DCHECK(SbWindowIsValid(window_));
+}
+
+InputEventsGenerator::~InputEventsGenerator() {}
+
+// For a left joystick, AMOTION_EVENT_AXIS_X reports the absolute X position of
+// the joystick. The value is normalized to a range from -1.0 (left) to 1.0
+// (right).
+//
+// For a left joystick, AMOTION_EVENT_AXIS_Y reports the absolute Y position of
+// the joystick. The value is normalized to a range from -1.0 (up or far) to 1.0
+// (down or near).
+//
+// On game pads with two analog joysticks, AMOTION_EVENT_AXIS_Z is often
+// reinterpreted to report the absolute X position of the second joystick.
+//
+// On game pads with two analog joysticks, AMOTION_EVENT_AXIS_RZ is often
+// reinterpreted to report the absolute Y position of the second joystick.
+void InputEventsGenerator::ProcessJoyStickEvent(FlatAxis axis,
+ int32_t motion_axis,
+ AInputEvent* android_event,
+ Events* events) {
+ SB_DCHECK(AMotionEvent_getPointerCount(android_event) > 0);
+
+ int32_t device_id = AInputEvent_getDeviceId(android_event);
+ SB_DCHECK(device_flat_.find(device_id) != device_flat_.end());
+
+ float flat = device_flat_[device_id][axis];
+ float offset = AMotionEvent_getAxisValue(android_event, motion_axis, 0);
+ int sign = offset < 0.0f ? -1 : 1;
+
+ if (SbDoubleAbsolute(offset) < flat) {
+ offset = sign * flat;
+ }
+ // Rescaled the range:
+ // [-1.0f, -flat] to [-1.0f, 0.0f] and [flat, 1.0f] to [0.0f, 1.0f]
+ offset = (offset - sign * flat) / (1 - flat);
+
+ // Report up and left as negative values.
+ SbInputVector input_vector;
+ SbKey key = kSbKeyUnknown;
+ SbKeyLocation location = kSbKeyLocationUnspecified;
+ switch (axis) {
+ case kLeftX: {
+ input_vector.x = offset;
+ input_vector.y = 0.0f;
+ key = kSbKeyGamepadLeftStickLeft;
+ location = kSbKeyLocationLeft;
+ break;
+ }
+ case kLeftY: {
+ input_vector.x = 0.0f;
+ input_vector.y = offset;
+ key = kSbKeyGamepadLeftStickUp;
+ location = kSbKeyLocationLeft;
+ break;
+ }
+ case kRightX: {
+ input_vector.x = offset;
+ input_vector.y = 0.0f;
+ key = kSbKeyGamepadRightStickLeft;
+ location = kSbKeyLocationRight;
+ break;
+ }
+ case kRightY: {
+ input_vector.x = 0.0f;
+ input_vector.y = offset;
+ key = kSbKeyGamepadRightStickUp;
+ location = kSbKeyLocationRight;
+ break;
+ }
+ default:
+ SB_NOTREACHED();
+ }
+
+ events->push_back(
+ CreateMoveEventWithKey(device_id, window_, key, location, input_vector));
+}
+
+namespace {
+
+// Generate a Starboard event from an Android event, with the SbKey and
+// SbInputEventType pre-specified (so that it can be used by event
+// synthesization as well.)
+void PushKeyEvent(SbKey key,
+ SbInputEventType type,
+ SbWindow window,
+ AInputEvent* android_event,
+ Events* events) {
+ if (key == kSbKeyUnknown) {
+ SB_NOTREACHED();
+ return;
+ }
+
+ std::unique_ptr<SbInputData> data(new SbInputData());
+ SbMemorySet(data.get(), 0, sizeof(*data));
+
+ // window
+ data->window = window;
+ data->type = type;
+
+ // device
+ // TODO: differentiate gamepad, remote, etc.
+ data->device_type = kSbInputDeviceTypeKeyboard;
+ data->device_id = AInputEvent_getDeviceId(android_event);
+
+ // key
+ data->key = key;
+ data->key_location = AInputEventToSbKeyLocation(android_event);
+ data->key_modifiers = AInputEventToSbModifiers(android_event);
+
+ std::unique_ptr<Event> event(
+ new Event(kSbEventTypeInput, data.release(),
+ &Application::DeleteDestructor<SbInputData>));
+ events->push_back(std::move(event));
+}
+
+// Some helper enumerations to index into the InputEventsGenerator::hat_value_
+// array.
+enum HatAxis {
+ kHatX,
+ kHatY,
+};
+
+struct HatValue {
+ HatAxis axis;
+ float value;
+};
+
+// Converts Starboard DPad direction keys to Starboard left thumbstick
+// direction keys.
+SbKey ConvertDPadKeyToThumbstickKey(SbKey key) {
+ switch (key) {
+ case kSbKeyGamepadDPadUp:
+ return kSbKeyGamepadLeftStickUp;
+ case kSbKeyGamepadDPadDown:
+ return kSbKeyGamepadLeftStickDown;
+ case kSbKeyGamepadDPadLeft:
+ return kSbKeyGamepadLeftStickLeft;
+ case kSbKeyGamepadDPadRight:
+ return kSbKeyGamepadLeftStickRight;
+ default: {
+ SB_NOTREACHED();
+ return kSbKeyUnknown;
+ }
+ }
+}
+
+// Convert a Starboard DPad direction key to a (axis, direction) pair.
+HatValue HatValueForDPadKey(SbKey key) {
+ SB_DCHECK(IsDPadKey(key));
+
+ switch (key) {
+ case kSbKeyGamepadDPadUp:
+ return HatValue({kHatY, -1.0f});
+ case kSbKeyGamepadDPadDown:
+ return HatValue({kHatY, 1.0f});
+ case kSbKeyGamepadDPadLeft:
+ return HatValue({kHatX, -1.0f});
+ case kSbKeyGamepadDPadRight:
+ return HatValue({kHatX, 1.0f});
+ default: {
+ SB_NOTREACHED();
+ return HatValue({kHatX, 0.0f});
+ }
+ }
+}
+
+// The inverse of HatValueForDPadKey().
+SbKey KeyForHatValue(const HatValue& hat_value) {
+ SB_DCHECK(hat_value.value > 0.5f || hat_value.value < -0.5f);
+ if (hat_value.axis == kHatX) {
+ if (hat_value.value > 0.5f) {
+ return kSbKeyGamepadDPadRight;
+ } else {
+ return kSbKeyGamepadDPadLeft;
+ }
+ } else if (hat_value.axis == kHatY) {
+ if (hat_value.value > 0.5f) {
+ return kSbKeyGamepadDPadDown;
+ } else {
+ return kSbKeyGamepadDPadUp;
+ }
+ } else {
+ SB_NOTREACHED();
+ return kSbKeyUnknown;
+ }
+}
+
+// Analyzes old axis values and new axis values and fire off any synthesized
+// key press/unpress events as necessary.
+void PossiblySynthesizeHatKeyEvents(HatAxis axis,
+ float old_value,
+ float new_value,
+ SbWindow window,
+ AInputEvent* android_event,
+ Events* events) {
+ if (old_value == new_value) {
+ // No events to generate if the hat motion value did not change.
+ return;
+ }
+
+ if (old_value > 0.5f || old_value < -0.5f) {
+ PushKeyEvent(KeyForHatValue(HatValue({axis, old_value})),
+ kSbInputEventTypeUnpress, window, android_event, events);
+ }
+ if (new_value > 0.5f || new_value < -0.5f) {
+ PushKeyEvent(KeyForHatValue(HatValue({axis, new_value})),
+ kSbInputEventTypePress, window, android_event, events);
+ }
+}
+
+} // namespace
+
+bool InputEventsGenerator::ProcessKeyEvent(AInputEvent* android_event,
+ Events* events) {
+#ifdef STARBOARD_INPUT_EVENTS_FILTER
+ if (!input_events_filter_.ShouldProcessKeyEvent(android_event)) {
+ return false;
+ }
+#endif
+
+ SbInputEventType type;
+ switch (AKeyEvent_getAction(android_event)) {
+ case AKEY_EVENT_ACTION_DOWN:
+ type = kSbInputEventTypePress;
+ break;
+ case AKEY_EVENT_ACTION_UP:
+ type = kSbInputEventTypeUnpress;
+ break;
+ default:
+ // TODO: send multiple events for AKEY_EVENT_ACTION_MULTIPLE
+ return false;
+ }
+
+ SbKey key = AInputEventToSbKey(android_event);
+ if (key == kSbKeyUnknown) {
+ return false;
+ }
+
+ if (AKeyEvent_getFlags(android_event) & AKEY_EVENT_FLAG_FALLBACK &&
+ IsDPadKey(key)) {
+ // For fallback DPad keys, we flow into special processing to manage the
+ // differentiation between the actual DPad and the left thumbstick, since
+ // Android conflates the key down/up events for these inputs.
+ ProcessFallbackDPadEvent(type, key, android_event, events);
+ } else {
+ PushKeyEvent(key, type, window_, android_event, events);
+ }
+ return true;
+}
+
+namespace {
+
+SbKey ButtonStateToSbKey(int32_t button_state) {
+ if (button_state & AMOTION_EVENT_BUTTON_PRIMARY) {
+ return kSbKeyMouse1;
+ } else if (button_state & AMOTION_EVENT_BUTTON_SECONDARY) {
+ return kSbKeyMouse2;
+ } else if (button_state & AMOTION_EVENT_BUTTON_TERTIARY) {
+ return kSbKeyMouse3;
+ } else if (button_state & AMOTION_EVENT_BUTTON_BACK) {
+ return kSbKeyBrowserBack;
+ } else if (button_state & AMOTION_EVENT_BUTTON_FORWARD) {
+ return kSbKeyBrowserForward;
+ }
+ return kSbKeyUnknown;
+}
+
+// Get an SbKeyModifiers from a button state
+unsigned int ButtonStateToSbModifiers(unsigned int button_state) {
+ unsigned int key_modifiers = kSbKeyModifiersNone;
+#if SB_API_VERSION >= 6
+ if (button_state & AMOTION_EVENT_BUTTON_PRIMARY) {
+ key_modifiers |= kSbKeyModifiersPointerButtonLeft;
+ }
+ if (button_state & AMOTION_EVENT_BUTTON_SECONDARY) {
+ key_modifiers |= kSbKeyModifiersPointerButtonMiddle;
+ }
+ if (button_state & AMOTION_EVENT_BUTTON_TERTIARY) {
+ key_modifiers |= kSbKeyModifiersPointerButtonRight;
+ }
+ if (button_state & AMOTION_EVENT_BUTTON_BACK) {
+ key_modifiers |= kSbKeyModifiersPointerButtonBack;
+ }
+ if (button_state & AMOTION_EVENT_BUTTON_FORWARD) {
+ key_modifiers |= kSbKeyModifiersPointerButtonForward;
+ }
+#endif
+ return key_modifiers;
+}
+
+#if SB_API_VERSION < 6
+SbKey ScrollAxisToKey(float hscroll, float vscroll) {
+ if (vscroll != 0) {
+ return vscroll < 0 ? kSbKeyDown : kSbKeyUp;
+ } else if (hscroll != 0) {
+ return hscroll > 0 ? kSbKeyLeft : kSbKeyRight;
+ }
+ return kSbKeyUnknown;
+}
+#endif
+
+} // namespace
+
+bool InputEventsGenerator::ProcessPointerEvent(AInputEvent* android_event,
+ Events* events) {
+ float offset_x =
+ AMotionEvent_getAxisValue(android_event, AMOTION_EVENT_AXIS_X, 0);
+ float offset_y =
+ AMotionEvent_getAxisValue(android_event, AMOTION_EVENT_AXIS_Y, 0);
+
+ std::unique_ptr<SbInputData> data(new SbInputData());
+ SbMemorySet(data.get(), 0, sizeof(*data));
+
+ data->window = window_;
+ SB_DCHECK(SbWindowIsValid(data->window));
+#if SB_API_VERSION >= 6
+ data->pressure = NAN;
+ data->size = {NAN, NAN};
+ data->tilt = {NAN, NAN};
+#endif
+ unsigned int button_state = AMotionEvent_getButtonState(android_event);
+ unsigned int button_modifiers = ButtonStateToSbModifiers(button_state);
+
+ // Default to reporting pointer events as mouse events.
+ data->device_type = kSbInputDeviceTypeMouse;
+
+ // Report both stylus and touchscreen events as touchscreen device events.
+ int32_t event_source = AInputEvent_getSource(android_event);
+ if (((event_source & AINPUT_SOURCE_TOUCHSCREEN) != 0) ||
+ ((event_source & AINPUT_SOURCE_STYLUS) != 0)) {
+ data->device_type = kSbInputDeviceTypeTouchScreen;
+ }
+
+ data->device_id = AInputEvent_getDeviceId(android_event);
+ data->key_modifiers =
+ button_modifiers | AInputEventToSbModifiers(android_event);
+ data->position.x = offset_x;
+ data->position.y = offset_y;
+ data->key = ButtonStateToSbKey(button_state);
+
+ switch (AKeyEvent_getAction(android_event) & AMOTION_EVENT_ACTION_MASK) {
+ case AMOTION_EVENT_ACTION_UP:
+ data->type = kSbInputEventTypeUnpress;
+ break;
+ case AMOTION_EVENT_ACTION_DOWN:
+ data->type = kSbInputEventTypePress;
+ break;
+ case AMOTION_EVENT_ACTION_MOVE:
+ case AMOTION_EVENT_ACTION_HOVER_MOVE:
+ data->type = kSbInputEventTypeMove;
+ break;
+ case AMOTION_EVENT_ACTION_SCROLL: {
+ float hscroll = AMotionEvent_getAxisValue(
+ android_event, AMOTION_EVENT_AXIS_HSCROLL, 0); // left is -1
+ float vscroll = AMotionEvent_getAxisValue(
+ android_event, AMOTION_EVENT_AXIS_VSCROLL, 0); // down is -1
+ float wheel =
+ AMotionEvent_getAxisValue(android_event, AMOTION_EVENT_AXIS_WHEEL, 0);
+#if SB_API_VERSION >= 6
+ data->type = kSbInputEventTypeWheel;
+ data->key = kSbKeyUnknown;
+ data->delta.y = -vscroll;
+ data->delta.x = hscroll;
+#else
+ // This version of Starboard does not support wheel event types, send
+ // keyboard event types instead.
+ data->device_type = kSbInputDeviceTypeKeyboard;
+ data->key = ScrollAxisToKey(hscroll, vscroll);
+
+ std::unique_ptr<SbInputData> data_press(new SbInputData());
+ SbMemoryCopy(data_press.get(), data.get(), sizeof(*data_press));
+
+ // Send a press and unpress event.
+ data_press->type = kSbInputEventTypePress;
+ events->push_back(std::unique_ptr<Event>(
+ new Application::Event(kSbEventTypeInput, data_press.release(),
+ &Application::DeleteDestructor<SbInputData>)));
+
+ data->type = kSbInputEventTypeUnpress;
+#endif
+ break;
+ }
+ default:
+ return false;
+ }
+
+ events->push_back(std::unique_ptr<Event>(
+ new Application::Event(kSbEventTypeInput, data.release(),
+ &Application::DeleteDestructor<SbInputData>)));
+ return true;
+}
+
+bool InputEventsGenerator::ProcessMotionEvent(AInputEvent* android_event,
+ Events* events) {
+ int32_t event_source = AInputEvent_getSource(android_event);
+ if ((event_source & AINPUT_SOURCE_CLASS_POINTER) != 0) {
+ return ProcessPointerEvent(android_event, events);
+ }
+ if ((event_source & AINPUT_SOURCE_JOYSTICK) == 0) {
+ // Only handles joystick events in the code below.
+ return false;
+ }
+
+ UpdateDeviceFlatMapIfNecessary(android_event);
+ ProcessJoyStickEvent(kLeftX, AMOTION_EVENT_AXIS_X, android_event, events);
+ ProcessJoyStickEvent(kLeftY, AMOTION_EVENT_AXIS_Y, android_event, events);
+ ProcessJoyStickEvent(kRightX, AMOTION_EVENT_AXIS_Z, android_event, events);
+ ProcessJoyStickEvent(kRightY, AMOTION_EVENT_AXIS_RZ, android_event, events);
+
+ // Remember the "hat" input values (dpad on the game controller) to help
+ // differentiate hat vs. stick fallback events.
+ UpdateHatValuesAndPossiblySynthesizeKeyEvents(android_event, events);
+
+ // Lie to Android and tell it that we did not process the motion event,
+ // causing Android to synthesize dpad key events for us. When we handle
+ // those synthesized key events we'll enqueue kSbKeyGamepadLeft rather
+ // than kSbKeyGamepadDPad events if they're from the joystick.
+ return false;
+}
+
+// Special processing to disambiguate between DPad events and left-thumbstick
+// direction key events.
+void InputEventsGenerator::ProcessFallbackDPadEvent(SbInputEventType type,
+ SbKey key,
+ AInputEvent* android_event,
+ Events* events) {
+ SB_DCHECK(AKeyEvent_getFlags(android_event) & AKEY_EVENT_FLAG_FALLBACK);
+ SB_DCHECK(IsDPadKey(key));
+
+ HatAxis hat_axis = HatValueForDPadKey(key).axis;
+
+ if (hat_value_[hat_axis] != 0.0f && type == kSbInputEventTypePress) {
+ // Direction pad events are all assumed to be coming from the hat controls
+ // if motion events for that hat DPAD is active, but we do still handle
+ // repeat keys here.
+ if (AKeyEvent_getRepeatCount(android_event) > 0) {
+ SB_LOG(INFO) << AKeyEvent_getRepeatCount(android_event);
+ PushKeyEvent(key, kSbInputEventTypePress, window_, android_event, events);
+ }
+ return;
+ }
+
+ // If we get this far, then we are exclusively dealing with thumbstick events,
+ // as actual DPad events are processed in motion events by checking the
+ // hat axis representing the DPad.
+ SbKey thumbstick_key = ConvertDPadKeyToThumbstickKey(key);
+
+ if (left_thumbstick_key_pressed_[hat_axis] != kSbKeyUnknown &&
+ (type == kSbInputEventTypeUnpress ||
+ left_thumbstick_key_pressed_[hat_axis] != thumbstick_key)) {
+ // Fire an unpressed event if our current key differs from the last seen
+ // key.
+ PushKeyEvent(left_thumbstick_key_pressed_[hat_axis],
+ kSbInputEventTypeUnpress, window_, android_event, events);
+ }
+
+ if (type == kSbInputEventTypePress) {
+ PushKeyEvent(thumbstick_key, kSbInputEventTypePress, window_,
+ android_event, events);
+ left_thumbstick_key_pressed_[hat_axis] = thumbstick_key;
+ } else if (type == kSbInputEventTypeUnpress) {
+ left_thumbstick_key_pressed_[hat_axis] = kSbKeyUnknown;
+ } else {
+ SB_NOTREACHED();
+ }
+}
+
+// Update |InputEventsGenerator::hat_value_| according to the incoming motion
+// event's data. Possibly generate DPad events based on any changes in value
+// here.
+void InputEventsGenerator::UpdateHatValuesAndPossiblySynthesizeKeyEvents(
+ AInputEvent* android_event,
+ Events* events) {
+ float new_hat_x =
+ AMotionEvent_getAxisValue(android_event, AMOTION_EVENT_AXIS_HAT_X, 0);
+ PossiblySynthesizeHatKeyEvents(kHatX, hat_value_[kHatX], new_hat_x, window_,
+ android_event, events);
+ hat_value_[kHatX] = new_hat_x;
+
+ float new_hat_y =
+ AMotionEvent_getAxisValue(android_event, AMOTION_EVENT_AXIS_HAT_Y, 0);
+ PossiblySynthesizeHatKeyEvents(kHatY, hat_value_[kHatY], new_hat_y, window_,
+ android_event, events);
+ hat_value_[kHatY] = new_hat_y;
+}
+
+void InputEventsGenerator::UpdateDeviceFlatMapIfNecessary(
+ AInputEvent* android_event) {
+ int32_t device_id = AInputEvent_getDeviceId(android_event);
+ if (device_flat_.find(device_id) != device_flat_.end()) {
+ // |device_flat_| is already contains the device flat information.
+ return;
+ }
+
+ JniEnvExt* env = JniEnvExt::Get();
+ ScopedLocalJavaRef<jobject> input_device(env->CallStaticObjectMethodOrAbort(
+ "android/view/InputDevice", "getDevice", "(I)Landroid/view/InputDevice;",
+ device_id));
+ float flats[kNumAxes] = {GetFlat(input_device.Get(), AMOTION_EVENT_AXIS_X),
+ GetFlat(input_device.Get(), AMOTION_EVENT_AXIS_Y),
+ GetFlat(input_device.Get(), AMOTION_EVENT_AXIS_Z),
+ GetFlat(input_device.Get(), AMOTION_EVENT_AXIS_RZ)};
+ device_flat_[device_id] = std::vector<float>(flats, flats + kNumAxes);
+}
+
+bool InputEventsGenerator::CreateInputEventsFromAndroidEvent(
+ AInputEvent* android_event,
+ Events* events) {
+ if (android_event == NULL ||
+ (AInputEvent_getType(android_event) != AINPUT_EVENT_TYPE_KEY &&
+ AInputEvent_getType(android_event) != AINPUT_EVENT_TYPE_MOTION)) {
+ return false;
+ }
+
+ switch (AInputEvent_getType(android_event)) {
+ case AINPUT_EVENT_TYPE_KEY:
+ return ProcessKeyEvent(android_event, events);
+ case AINPUT_EVENT_TYPE_MOTION: {
+ return ProcessMotionEvent(android_event, events);
+ }
+ default:
+ SB_NOTREACHED();
+ }
+
+ return false;
+}
+
+void InputEventsGenerator::CreateInputEventsFromSbKey(SbKey key,
+ Events* events) {
+ events->clear();
+
+ // Press event
+ std::unique_ptr<SbInputData> data(new SbInputData());
+ SbMemorySet(data.get(), 0, sizeof(*data));
+
+ data->window = window_;
+ data->type = kSbInputEventTypePress;
+
+ data->device_type = kSbInputDeviceTypeKeyboard;
+ data->device_id = 0;
+
+ data->key = key;
+ data->key_location = kSbKeyLocationUnspecified;
+ data->key_modifiers = kSbKeyModifiersNone;
+
+ events->push_back(std::unique_ptr<Event>(
+ new Application::Event(kSbEventTypeInput, data.release(),
+ &Application::DeleteDestructor<SbInputData>)));
+
+ // Unpress event
+ data.reset(new SbInputData());
+ SbMemorySet(data.get(), 0, sizeof(*data));
+
+ data->window = window_;
+ data->type = kSbInputEventTypeUnpress;
+
+ data->device_type = kSbInputDeviceTypeKeyboard;
+ data->device_id = 0;
+
+ data->key = key;
+ data->key_location = kSbKeyLocationUnspecified;
+ data->key_modifiers = kSbKeyModifiersNone;
+
+ events->push_back(std::unique_ptr<Event>(
+ new Application::Event(kSbEventTypeInput, data.release(),
+ &Application::DeleteDestructor<SbInputData>)));
+}
+
+} // namespace shared
+} // namespace android
+} // namespace starboard
diff --git a/src/starboard/android/shared/input_events_generator.h b/src/starboard/android/shared/input_events_generator.h
new file mode 100644
index 0000000..a3f6aef
--- /dev/null
+++ b/src/starboard/android/shared/input_events_generator.h
@@ -0,0 +1,101 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_INPUT_EVENTS_GENERATOR_H_
+#define STARBOARD_ANDROID_SHARED_INPUT_EVENTS_GENERATOR_H_
+
+#include <android/input.h>
+#include <map>
+#include <memory>
+#include <vector>
+
+#ifdef STARBOARD_INPUT_EVENTS_FILTER
+#include "starboard/android/shared/input_events_filter.h"
+#endif
+
+#include "starboard/input.h"
+#include "starboard/shared/starboard/application.h"
+#include "starboard/window.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+class InputEventsGenerator {
+ public:
+ typedef ::starboard::shared::starboard::Application::Event Event;
+ typedef std::vector<std::unique_ptr<Event> > Events;
+
+ explicit InputEventsGenerator(SbWindow window);
+ virtual ~InputEventsGenerator();
+
+ // Translates an Android input event into a series of Starboard application
+ // events. The caller owns the new events and must delete them when done with
+ // them.
+ bool CreateInputEventsFromAndroidEvent(AInputEvent* android_event,
+ Events* events);
+
+ // Create press/unpress events from SbKey
+ // (for use with CobaltA11yHelper injection)
+ void CreateInputEventsFromSbKey(SbKey key, Events* events);
+
+ private:
+ enum FlatAxis {
+ kLeftX,
+ kLeftY,
+ kRightX,
+ kRightY,
+ kNumAxes,
+ };
+
+ bool ProcessKeyEvent(AInputEvent* android_event, Events* events);
+ bool ProcessPointerEvent(AInputEvent* android_event, Events* events);
+ bool ProcessMotionEvent(AInputEvent* android_event, Events* events);
+ void ProcessJoyStickEvent(FlatAxis axis,
+ int32_t motion_axis,
+ AInputEvent* android_event,
+ Events* events);
+ void UpdateDeviceFlatMapIfNecessary(AInputEvent* android_event);
+
+ void ProcessFallbackDPadEvent(SbInputEventType type,
+ SbKey key,
+ AInputEvent* android_event,
+ Events* events);
+ void UpdateHatValuesAndPossiblySynthesizeKeyEvents(AInputEvent* android_event,
+ Events* events);
+
+ SbWindow window_;
+
+#ifdef STARBOARD_INPUT_EVENTS_FILTER
+ InputEventsFilter input_events_filter_;
+#endif
+
+ // Map the device id with joystick flat position.
+ // Cache the flat area of joystick to avoid calling jni functions frequently.
+ std::map<int32_t, std::vector<float> > device_flat_;
+
+ // The curent X/Y analog values of the "hat" (dpad on the game controller).
+ float hat_value_[2];
+
+ // The last known value of the left thumbstick, used to track when we should
+ // generate key unpressed events for it. We store values for horizontal and
+ // vertical directions independently.
+ SbKey left_thumbstick_key_pressed_[2];
+};
+
+} // namespace shared
+} // namespace android
+} // namespace starboard
+
+#endif // STARBOARD_ANDROID_SHARED_INPUT_EVENTS_GENERATOR_H_
diff --git a/src/starboard/android/shared/jni_env_ext.cc b/src/starboard/android/shared/jni_env_ext.cc
new file mode 100644
index 0000000..a42359e
--- /dev/null
+++ b/src/starboard/android/shared/jni_env_ext.cc
@@ -0,0 +1,107 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/jni_env_ext.h"
+
+#include <android/native_activity.h>
+#include <jni.h>
+
+#include <algorithm>
+#include <string>
+
+#include "starboard/thread.h"
+
+namespace {
+
+SbThreadLocalKey g_tls_key = kSbThreadLocalKeyInvalid;
+JavaVM* g_vm = NULL;
+jobject g_application_class_loader = NULL;
+jobject g_starboard_bridge = NULL;
+
+void Destroy(void* value) {
+ // OnThreadShutdown() must be called on each thread before it is destroyed.
+ SB_DCHECK(value == NULL);
+}
+
+} // namespace
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+// static
+void JniEnvExt::Initialize(JniEnvExt* env, jobject starboard_bridge) {
+ SB_DCHECK(g_tls_key == kSbThreadLocalKeyInvalid);
+ g_tls_key = SbThreadCreateLocalKey(Destroy);
+
+ SB_DCHECK(g_vm == NULL);
+ env->GetJavaVM(&g_vm);
+
+ SB_DCHECK(g_application_class_loader == NULL);
+ g_application_class_loader = env->ConvertLocalRefToGlobalRef(
+ env->CallObjectMethodOrAbort(env->GetObjectClass(starboard_bridge),
+ "getClassLoader",
+ "()Ljava/lang/ClassLoader;"));
+
+ SB_DCHECK(g_starboard_bridge == NULL);
+ g_starboard_bridge = env->NewGlobalRef(starboard_bridge);
+}
+
+// static
+void JniEnvExt::OnThreadShutdown() {
+ // We must call DetachCurrentThread() before exiting, if we have ever
+ // previously called AttachCurrentThread() on it.
+ // http://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/invocation.html
+ if (SbThreadGetLocalValue(g_tls_key)) {
+ g_vm->DetachCurrentThread();
+ SbThreadSetLocalValue(g_tls_key, NULL);
+ }
+}
+
+JniEnvExt* JniEnvExt::Get() {
+ JNIEnv* env;
+ if (JNI_OK != g_vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6)) {
+ // Tell the JVM our thread name so it doesn't change it.
+ char thread_name[16];
+ SbThreadGetName(thread_name, sizeof(thread_name));
+ JavaVMAttachArgs args { JNI_VERSION_1_6, thread_name, NULL };
+ g_vm->AttachCurrentThread(&env, &args);
+ // We don't use the value, but any non-NULL means we have to detach.
+ SbThreadSetLocalValue(g_tls_key, env);
+ }
+ // The downcast is safe since we only add methods, not fields.
+ return static_cast<JniEnvExt*>(env);
+}
+
+jobject JniEnvExt::GetStarboardBridge() {
+ return g_starboard_bridge;
+}
+
+jclass JniEnvExt::FindClassExtOrAbort(const char* name) {
+ // Convert the JNI FindClass name with slashes to the "binary name" with dots
+ // for ClassLoader.loadClass().
+ ::std::string dot_name = name;
+ ::std::replace(dot_name.begin(), dot_name.end(), '/', '.');
+ jstring jname = NewStringUTF(dot_name.c_str());
+ AbortOnException();
+ jobject clazz_obj =
+ CallObjectMethodOrAbort(g_application_class_loader, "loadClass",
+ "(Ljava/lang/String;)Ljava/lang/Class;", jname);
+ DeleteLocalRef(jname);
+ return static_cast<jclass>(clazz_obj);
+}
+
+} // namespace shared
+} // namespace android
+} // namespace starboard
diff --git a/src/starboard/android/shared/jni_env_ext.h b/src/starboard/android/shared/jni_env_ext.h
new file mode 100644
index 0000000..3797c08
--- /dev/null
+++ b/src/starboard/android/shared/jni_env_ext.h
@@ -0,0 +1,387 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_JNI_ENV_EXT_H_
+#define STARBOARD_ANDROID_SHARED_JNI_ENV_EXT_H_
+
+#include <android/native_activity.h>
+#include <jni.h>
+
+#include <cstdarg>
+#include <cstring>
+#include <string>
+
+#include "starboard/log.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+// An extension to JNIEnv to simplify making JNI calls.
+//
+// Call the static Get() method to get an instance that is already attached to
+// the JVM in the current thread.
+//
+// This extends the JNIEnv structure, which already has a C++ interface for
+// calling JNI methods, so any JNIEnv method can be called directly on this.
+//
+// There are convenience methods to lookup and call Java methods on object
+// instances in a single step, with even simpler methods to call Java methods on
+// the StarboardBridge.
+struct JniEnvExt : public JNIEnv {
+ // One-time initialization to be called before starting the application.
+ static void Initialize(JniEnvExt* jni_env, jobject starboard_bridge);
+
+ // Called right before each native thread is about to be shutdown.
+ static void OnThreadShutdown();
+
+ // Returns the thread-specific instance of JniEnvExt.
+ static JniEnvExt* Get();
+
+ // Returns the StarboardBridge object.
+ jobject GetStarboardBridge();
+
+ // Lookup the class of an object and find a field in it.
+ jfieldID GetStaticFieldIDOrAbort(jclass clazz,
+ const char* name,
+ const char* sig) {
+ jfieldID field = GetStaticFieldID(clazz, name, sig);
+ AbortOnException();
+ return field;
+ }
+
+ jfieldID GetFieldIDOrAbort(jobject obj,
+ const char* name,
+ const char* sig) {
+ jclass clazz = GetObjectClass(obj);
+ AbortOnException();
+ jfieldID field = GetFieldID(clazz, name, sig);
+ AbortOnException();
+ DeleteLocalRef(clazz);
+ return field;
+ }
+
+ jint GetEnumValueOrAbort(jclass clazz, const char* name) {
+ jfieldID field = GetStaticFieldIDOrAbort(clazz, name, "I");
+ jint enum_value = GetStaticIntField(clazz, field);
+ AbortOnException();
+ return enum_value;
+ }
+
+ // Lookup the class of an object and find a method in it.
+ jmethodID GetObjectMethodIDOrAbort(jobject obj,
+ const char* name,
+ const char* sig) {
+ jclass clazz = GetObjectClass(obj);
+ AbortOnException();
+ jmethodID method_id = GetMethodID(clazz, name, sig);
+ AbortOnException();
+ DeleteLocalRef(clazz);
+ return method_id;
+ }
+
+ jmethodID GetStaticMethodIDOrAbort(jclass clazz,
+ const char* name,
+ const char* sig) {
+ jmethodID method = GetStaticMethodID(clazz, name, sig);
+ AbortOnException();
+ return method;
+ }
+
+ jobject GetObjectArrayElementOrAbort(jobjectArray array, jsize index) {
+ jobject result = GetObjectArrayElement(array, index);
+ AbortOnException();
+ return result;
+ }
+
+ // Find a class by name using the application's class loader. This can load
+ // both system classes and application classes, even when not in a JNI
+ // stack frame (e.g. in a native thread that was attached the the JVM).
+ // https://developer.android.com/training/articles/perf-jni.html#faq_FindClass
+ jclass FindClassExtOrAbort(const char* name);
+
+ jclass FindClassOrAbort(const char* name) {
+ jclass result = FindClass(name);
+ AbortOnException();
+ return result;
+ }
+
+ // Convenience method to lookup and call a constructor.
+ jobject NewObjectOrAbort(const char* class_name, const char* sig, ...) {
+ va_list argp;
+ va_start(argp, sig);
+ jclass clazz = FindClassExtOrAbort(class_name);
+ jmethodID methodID = GetMethodID(clazz, "<init>", sig);
+ AbortOnException();
+ jobject result = NewObjectV(clazz, methodID, argp);
+ AbortOnException();
+ DeleteLocalRef(clazz);
+ va_end(argp);
+ return result;
+ }
+
+ // Constructs a new java.lang.String object from an array of characters in
+ // standard UTF-8 encoding. This differs from JNIEnv::NewStringUTF() which
+ // takes JNI modified UTF-8.
+ jstring NewStringStandardUTFOrAbort(const char* bytes) {
+ const jstring charset = NewStringUTF("UTF-8");
+ AbortOnException();
+ const jbyteArray byte_array = NewByteArrayFromRaw(
+ reinterpret_cast<const jbyte*>(bytes), strlen(bytes));
+ AbortOnException();
+ jstring result = static_cast<jstring>(NewObjectOrAbort(
+ "java/lang/String", "([BLjava/lang/String;)V", byte_array, charset));
+ DeleteLocalRef(byte_array);
+ DeleteLocalRef(charset);
+ return result;
+ }
+
+ // Returns a std::string representing the jstring in standard UTF-8 encoding.
+ // This differs from JNIEnv::GetStringUTFChars() which returns modified UTF-8.
+ // Also, the buffer of the returned bytes is managed by the std::string object
+ // so it is not necessary to release it with JNIEnv::ReleaseStringUTFChars().
+ std::string GetStringStandardUTFOrAbort(jstring str) {
+ if (str == NULL) {
+ return std::string();
+ }
+ const jstring charset = NewStringUTF("UTF-8");
+ AbortOnException();
+ const jbyteArray byte_array = static_cast<jbyteArray>(
+ CallObjectMethodOrAbort(str, "getBytes", "(Ljava/lang/String;)[B",
+ charset));
+ jsize array_length = GetArrayLength(byte_array);
+ AbortOnException();
+ void* bytes = GetPrimitiveArrayCritical(byte_array, NULL);
+ AbortOnException();
+ std::string result(static_cast<const char*>(bytes), array_length);
+ ReleasePrimitiveArrayCritical(byte_array, bytes, JNI_ABORT);
+ AbortOnException();
+ DeleteLocalRef(byte_array);
+ DeleteLocalRef(charset);
+ return result;
+ }
+
+// Convenience methods to lookup and read a field or call a method all at once:
+// Get[Type]FieldOrAbort() takes a jobject of an instance.
+// Call[Type]MethodOrAbort() takes a jobject of an instance.
+// CallStarboard[Type]MethodOrAbort() to call methods on the StarboardBridge.
+#define X(_jtype, _jname) \
+ _jtype Get##_jname##FieldOrAbort(jobject obj, const char* name, \
+ const char* sig) { \
+ _jtype result = Get##_jname##Field(obj, GetFieldIDOrAbort(obj, name, sig));\
+ AbortOnException(); \
+ return result; \
+ } \
+ \
+ _jtype GetStatic##_jname##FieldOrAbort(const char* class_name, \
+ const char* name, const char* sig) { \
+ jclass clazz = FindClassExtOrAbort(class_name); \
+ return GetStatic##_jname##FieldOrAbort(clazz, name, sig); \
+ } \
+ \
+ _jtype GetStatic##_jname##FieldOrAbort(jclass clazz, const char* name, \
+ const char* sig) { \
+ _jtype result = GetStatic##_jname##Field( \
+ clazz, GetStaticFieldIDOrAbort(clazz, name, sig)); \
+ AbortOnException(); \
+ return result; \
+ } \
+ \
+ _jtype Call##_jname##MethodOrAbort(jobject obj, const char* name, \
+ const char* sig, ...) { \
+ va_list argp; \
+ va_start(argp, sig); \
+ _jtype result = Call##_jname##MethodVOrAbort( \
+ obj, GetObjectMethodIDOrAbort(obj, name, sig), argp); \
+ va_end(argp); \
+ return result; \
+ } \
+ \
+ _jtype CallStarboard##_jname##MethodOrAbort(const char* name, \
+ const char* sig, ...) { \
+ va_list argp; \
+ va_start(argp, sig); \
+ jobject obj = GetStarboardBridge(); \
+ _jtype result = Call##_jname##MethodVOrAbort( \
+ obj, GetObjectMethodIDOrAbort(obj, name, sig), argp); \
+ va_end(argp); \
+ return result; \
+ } \
+ \
+ _jtype CallStatic##_jname##MethodOrAbort( \
+ const char* class_name, const char* method_name, const char* sig, ...) { \
+ va_list argp; \
+ va_start(argp, sig); \
+ jclass clazz = FindClassExtOrAbort(class_name); \
+ _jtype result = CallStatic##_jname##MethodVOrAbort( \
+ clazz, GetStaticMethodIDOrAbort(clazz, method_name, sig), argp); \
+ DeleteLocalRef(clazz); \
+ va_end(argp); \
+ return result; \
+ } \
+ \
+ _jtype Call##_jname##MethodVOrAbort(jobject obj, jmethodID methodID, \
+ va_list args) { \
+ _jtype result = Call##_jname##MethodV(obj, methodID, args); \
+ AbortOnException(); \
+ return result; \
+ } \
+ \
+ _jtype CallStatic##_jname##MethodVOrAbort(jclass clazz, jmethodID methodID, \
+ va_list args) { \
+ _jtype result = CallStatic##_jname##MethodV(clazz, methodID, args); \
+ AbortOnException(); \
+ return result; \
+ }
+
+ X(jobject, Object)
+ X(jboolean, Boolean)
+ X(jbyte, Byte)
+ X(jchar, Char)
+ X(jshort, Short)
+ X(jint, Int)
+ X(jlong, Long)
+ X(jfloat, Float)
+ X(jdouble, Double)
+
+#undef X
+
+ void CallVoidMethod(jobject obj, const char* name, const char* sig, ...) {
+ va_list argp;
+ va_start(argp, sig);
+ CallVoidMethodV(obj, GetObjectMethodIDOrAbort(obj, name, sig), argp);
+ va_end(argp);
+ }
+
+ void CallVoidMethodOrAbort(jobject obj,
+ const char* name,
+ const char* sig,
+ ...) {
+ va_list argp;
+ va_start(argp, sig);
+ CallVoidMethodVOrAbort(obj, GetObjectMethodIDOrAbort(obj, name, sig), argp);
+ va_end(argp);
+ }
+
+ void CallVoidMethodVOrAbort(jobject obj, jmethodID methodID, va_list args) {
+ CallVoidMethodV(obj, methodID, args);
+ AbortOnException();
+ }
+
+ void CallStarboardVoidMethod(const char* name, const char* sig, ...) {
+ va_list argp;
+ va_start(argp, sig);
+ jobject obj = GetStarboardBridge();
+ CallVoidMethodV(obj, GetObjectMethodIDOrAbort(obj, name, sig), argp);
+ va_end(argp);
+ }
+
+ void CallStarboardVoidMethodOrAbort(const char* name, const char* sig, ...) {
+ va_list argp;
+ va_start(argp, sig);
+ jobject obj = GetStarboardBridge();
+ CallVoidMethodVOrAbort(obj, GetObjectMethodIDOrAbort(obj, name, sig), argp);
+ va_end(argp);
+ }
+
+ void CallStaticVoidMethod(const char* class_name,
+ const char* method_name,
+ const char* sig,
+ ...) {
+ va_list argp;
+ va_start(argp, sig);
+ jclass clazz = FindClassExtOrAbort(class_name);
+ CallStaticVoidMethodV(
+ clazz, GetStaticMethodIDOrAbort(clazz, method_name, sig), argp);
+ DeleteLocalRef(clazz);
+ va_end(argp);
+ }
+
+ void CallStaticVoidMethodOrAbort(const char* class_name,
+ const char* method_name,
+ const char* sig,
+ ...) {
+ va_list argp;
+ va_start(argp, sig);
+ jclass clazz = FindClassExtOrAbort(class_name);
+ CallStaticVoidMethodV(
+ clazz, GetStaticMethodIDOrAbort(clazz, method_name, sig), argp);
+ AbortOnException();
+ DeleteLocalRef(clazz);
+ va_end(argp);
+ }
+
+ jstring GetStringFieldOrAbort(jobject obj, const char* name) {
+ return static_cast<jstring>(
+ GetObjectFieldOrAbort(obj, name, "Ljava/lang/String;"));
+ }
+
+ jstring GetStaticStringFieldOrAbort(const char* class_name,
+ const char* name) {
+ return static_cast<jstring>(
+ GetStaticObjectFieldOrAbort(class_name, name, "Ljava/lang/String;"));
+ }
+
+ jstring GetStaticStringFieldOrAbort(jclass clazz, const char* name) {
+ return static_cast<jstring>(
+ GetStaticObjectFieldOrAbort(clazz, name, "Ljava/lang/String;"));
+ }
+
+// Convenience method to create a j[Type]Array from raw, native data. It is
+// the responsibility of clients to free the returned array when done with it
+// by manually calling |DeleteLocalRef| on it.
+#define X(_jtype, _jname) \
+ _jtype##Array New##_jname##ArrayFromRaw(const _jtype* data, jsize size) { \
+ SB_DCHECK(data); \
+ SB_DCHECK(size >= 0); \
+ _jtype##Array j_array = New##_jname##Array(size); \
+ SB_CHECK(j_array) << "Out of memory making new array"; \
+ Set##_jname##ArrayRegion(j_array, 0, size, data); \
+ return j_array; \
+ }
+
+ X(jboolean, Boolean)
+ X(jbyte, Byte)
+ X(jchar, Char)
+ X(jshort, Short)
+ X(jint, Int)
+ X(jlong, Long)
+ X(jfloat, Float)
+ X(jdouble, Double)
+
+#undef X
+
+ jobject ConvertLocalRefToGlobalRef(jobject local) {
+ jobject global = NewGlobalRef(local);
+ DeleteLocalRef(local);
+ return global;
+ }
+
+ void AbortOnException() {
+ if (!ExceptionCheck()) {
+ return;
+ }
+ ExceptionDescribe();
+ SbSystemBreakIntoDebugger();
+ }
+};
+
+SB_COMPILE_ASSERT(sizeof(JNIEnv) == sizeof(JniEnvExt),
+ JniEnvExt_must_not_add_fields);
+
+} // namespace shared
+} // namespace android
+} // namespace starboard
+
+#endif // STARBOARD_ANDROID_SHARED_JNI_ENV_EXT_H_
diff --git a/src/starboard/android/shared/jni_env_ext_test.cc b/src/starboard/android/shared/jni_env_ext_test.cc
new file mode 100644
index 0000000..4c69b95
--- /dev/null
+++ b/src/starboard/android/shared/jni_env_ext_test.cc
@@ -0,0 +1,102 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <string>
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/configuration.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+namespace {
+
+// UTF-16, UTF-8, and Modified UTF-8 test strings, all "𐆖€£$"
+// 𐆖 U+10196 -> U16: D800 DD96 U8: F0 90 86 96 MU8: ED A0 80 ED B6 96
+// € U+020AC -> U16: 20AC U8: E2 82 AC MU8: E2 82 AC
+// £ U+000A3 -> U16: 00A3 U8: C2 A3 MU8: C2 A3
+// $ U+00024 -> U16: 0024 U8: 24 MU8: 24
+const char16_t kU16[] = u"\U00010196\u20AC\u00A3\u0024";
+const char kU8[] = "\xF0\x90\x86\x96\xE2\x82\xAC\xC2\xA3\x24";
+const char kMU8[] = "\xED\xA0\x80\xED\xB6\x96\xE2\x82\xAC\xC2\xA3\x24";
+
+// Subtract one from the array size to not count the null terminator.
+const int kU16Length = SB_ARRAY_SIZE(kU16) - 1;
+const int kU8Length = SB_ARRAY_SIZE(kU8) - 1;
+const int kMU8Length = SB_ARRAY_SIZE(kMU8) - 1;
+
+// Note: there is no test for getting the string back as modified UTF-8 since
+// on some Android devices GetStringUTFChars() may return standard UTF-8.
+// (e.g. Nexus Player returns modified UTF-8, but Shield returns standard UTF-8)
+// see: https://github.com/android-ndk/ndk/issues/283
+
+TEST(JniEnvExtTest, NewStringStandardUTF) {
+ JniEnvExt* env = JniEnvExt::Get();
+ jstring j_str = env->NewStringStandardUTFOrAbort(kU8);
+
+ EXPECT_EQ(kU16Length, env->GetStringLength(j_str));
+ const jchar* u16_chars = env->GetStringChars(j_str, NULL);
+ std::u16string u16_string(
+ reinterpret_cast<const char16_t*>(u16_chars), kU16Length);
+ EXPECT_EQ(std::u16string(kU16), u16_string);
+ env->ReleaseStringChars(j_str, u16_chars);
+}
+
+TEST(JniEnvExtTest, NewStringModifiedUTF) {
+ JniEnvExt* env = JniEnvExt::Get();
+ jstring j_str = env->NewStringUTF(kMU8);
+
+ EXPECT_EQ(kU16Length, env->GetStringLength(j_str));
+ const jchar* u16_chars = env->GetStringChars(j_str, NULL);
+ std::u16string u16_string(
+ reinterpret_cast<const char16_t*>(u16_chars), kU16Length);
+ EXPECT_EQ(std::u16string(kU16), u16_string);
+ env->ReleaseStringChars(j_str, u16_chars);
+}
+
+TEST(JniEnvExtTest, EmptyNewStringStandardUTF) {
+ JniEnvExt* env = JniEnvExt::Get();
+ jstring j_str = env->NewStringStandardUTFOrAbort("");
+
+ EXPECT_EQ(0, env->GetStringLength(j_str));
+}
+
+TEST(JniEnvExtTest, GetStringStandardUTF) {
+ JniEnvExt* env = JniEnvExt::Get();
+ jstring j_str =
+ env->NewString(reinterpret_cast<const jchar*>(kU16), kU16Length);
+
+ std::string str = env->GetStringStandardUTFOrAbort(j_str);
+ EXPECT_EQ(kU8Length, str.length());
+ EXPECT_EQ(std::string(kU8), str);
+ env->DeleteLocalRef(j_str);
+}
+
+TEST(JniEnvExtTest, EmptyGetStringStandardUTF) {
+ JniEnvExt* env = JniEnvExt::Get();
+ jchar empty[] = {};
+ jstring j_str = env->NewString(empty, 0);
+
+ std::string str = env->GetStringStandardUTFOrAbort(j_str);
+ EXPECT_EQ(0, str.length());
+ EXPECT_EQ(std::string(), str);
+ env->DeleteLocalRef(j_str);
+}
+
+} // namespace
+} // namespace shared
+} // namespace android
+} // namespace starboard
diff --git a/src/starboard/android/shared/jni_utils.h b/src/starboard/android/shared/jni_utils.h
new file mode 100644
index 0000000..0722504
--- /dev/null
+++ b/src/starboard/android/shared/jni_utils.h
@@ -0,0 +1,88 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_JNI_UTILS_H_
+#define STARBOARD_ANDROID_SHARED_JNI_UTILS_H_
+
+#include <jni.h>
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/configuration.h"
+#include "starboard/log.h"
+#include "starboard/memory.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+// Wrapper class to manage the lifetime of a local reference to Java type
+// |JT|. This is necessary for local references to |JT|s that are obtained in
+// native code that was not called into from Java, since they will otherwise
+// not be cleaned up.
+template <typename JT>
+class ScopedLocalJavaRef {
+ public:
+ explicit ScopedLocalJavaRef(jobject j_object = NULL)
+ : jt_(static_cast<JT>(j_object)) {}
+ ~ScopedLocalJavaRef() {
+ if (jt_) {
+ JniEnvExt::Get()->DeleteLocalRef(jt_);
+ jt_ = NULL;
+ }
+ }
+ JT Get() const { return jt_; }
+ void Reset(jobject j_object) {
+ if (jt_) {
+ JniEnvExt::Get()->DeleteLocalRef(jt_);
+ }
+ jt_ = static_cast<JT>(j_object);
+ }
+ operator bool() const { return jt_; }
+
+ private:
+ JT jt_;
+
+ SB_DISALLOW_COPY_AND_ASSIGN(ScopedLocalJavaRef);
+};
+
+// Convenience class to manage the lifetime of a local Java ByteBuffer
+// reference, and provide accessors to its properties.
+class ScopedJavaByteBuffer {
+ public:
+ explicit ScopedJavaByteBuffer(jobject j_byte_buffer)
+ : j_byte_buffer_(j_byte_buffer) {}
+ void* address() const {
+ return JniEnvExt::Get()->GetDirectBufferAddress(j_byte_buffer_.Get());
+ }
+ jint capacity() const {
+ return JniEnvExt::Get()->GetDirectBufferCapacity(j_byte_buffer_.Get());
+ }
+ bool IsNull() const { return !j_byte_buffer_ || !address(); }
+ void CopyInto(const void* source, jint count) {
+ SB_DCHECK(!IsNull());
+ SB_DCHECK(count >= 0 && count <= capacity());
+ SbMemoryCopy(address(), source, count);
+ }
+
+ private:
+ ScopedLocalJavaRef<jobject> j_byte_buffer_;
+
+ SB_DISALLOW_COPY_AND_ASSIGN(ScopedJavaByteBuffer);
+};
+
+} // namespace shared
+} // namespace android
+} // namespace starboard
+
+#endif // STARBOARD_ANDROID_SHARED_JNI_UTILS_H_
diff --git a/src/starboard/android/shared/launcher.py b/src/starboard/android/shared/launcher.py
new file mode 100644
index 0000000..bd43320
--- /dev/null
+++ b/src/starboard/android/shared/launcher.py
@@ -0,0 +1,396 @@
+#
+# Copyright 2017 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""Android implementation of Starboard launcher abstraction."""
+
+import os
+import Queue
+import re
+import socket
+import subprocess
+import sys
+import threading
+import time
+
+import _env # pylint: disable=unused-import
+
+from starboard.android.shared import sdk_utils
+from starboard.tools import abstract_launcher
+
+_APP_PACKAGE_NAME = 'dev.cobalt.coat'
+
+_APP_START_INTENT = 'dev.cobalt.coat/dev.cobalt.app.MainActivity'
+
+# Matches an "adb shell am monitor" error line.
+_RE_ADB_AM_MONITOR_ERROR = re.compile(r'\*\* ERROR')
+
+# String added to queue to indicate process has crashed
+_QUEUE_CODE_CRASHED = 'crashed'
+
+# Args to go/crow, which is started if no other device is attached.
+_CROW_COMMANDLINE = ['/google/data/ro/teams/mobile_eng_prod/crow/crow.par',
+ '--api_level', '24', '--device', 'tv',
+ '--open_gl_driver', 'host',
+ '--noenable_g3_monitor']
+
+# How long to keep logging after a crash in order to emit the stack trace.
+_CRASH_LOG_SECONDS = 1.0
+
+_DEV_NULL = open('/dev/null')
+
+_ADB = os.path.join(sdk_utils.GetSdkPath(), 'platform-tools', 'adb')
+
+_RUNTIME_PERMISSIONS = [
+ 'android.permission.GET_ACCOUNTS',
+ 'android.permission.RECORD_AUDIO',
+]
+
+
+def TargetOsPathJoin(*path_elements):
+ """os.path.join for the target (Android)."""
+ return '/'.join(path_elements)
+
+
+def CleanLine(line):
+ """Removes trailing carriages returns from ADB output."""
+ return line.replace('\r', '')
+
+
+class StepTimer(object):
+ """Class for timing how long install/run steps take."""
+
+ def __init__(self, step_name):
+ self.step_name = step_name
+ self.start_time = time.time()
+ self.end_time = None
+
+ def Stop(self):
+ if self.start_time is None:
+ sys.stderr.write('Cannot stop timer; not started\n')
+ else:
+ self.end_time = time.time()
+ total_time = self.end_time - self.start_time
+ sys.stderr.write('Step \"{}\" took {} seconds.\n'.format(
+ self.step_name, total_time))
+
+
+class AdbCommandBuilder(object):
+ """Builder for 'adb' commands."""
+
+ def __init__(self, device_id):
+ self.device_id = device_id
+
+ def Build(self, *args):
+ """Builds an 'adb' commandline with the given args."""
+ result = [_ADB]
+ if self.device_id:
+ result.append('-s')
+ result.append(self.device_id)
+ result += list(args)
+ return result
+
+
+class AdbAmMonitorWatcher(object):
+ """Watches an "adb shell am monitor" process to detect crashes."""
+
+ def __init__(self, adb_builder, done_queue):
+ self.adb_builder = adb_builder
+ self.process = subprocess.Popen(
+ adb_builder.Build('shell', 'am', 'monitor'),
+ stdout=subprocess.PIPE,
+ stderr=_DEV_NULL,
+ close_fds=True)
+ self.thread = threading.Thread(target=self._Run)
+ self.thread.start()
+ self.done_queue = done_queue
+
+ def Shutdown(self):
+ self.process.kill()
+ self.thread.join()
+
+ def _Run(self):
+ while True:
+ line = CleanLine(self.process.stdout.readline())
+ if not line:
+ return
+ if re.search(_RE_ADB_AM_MONITOR_ERROR, line):
+ self.done_queue.put(_QUEUE_CODE_CRASHED)
+ # This log line will wake up the main thread
+ subprocess.call(
+ self.adb_builder.Build('shell', 'log', '-t', 'starboard',
+ 'am monitor detected crash'),
+ close_fds=True)
+
+
+class Launcher(abstract_launcher.AbstractLauncher):
+ """Run an application on Android."""
+
+ def __init__(self, platform, target_name, config, device_id, **kwargs):
+
+ super(Launcher, self).__init__(platform, target_name, config, device_id,
+ **kwargs)
+
+ if not self.device_id:
+ self.device_id = self._IdentifyDevice()
+ else:
+ self._ConnectIfNecessary()
+
+ self.adb_builder = AdbCommandBuilder(self.device_id)
+
+ out_directory = os.path.split(self.GetTargetPath())[0]
+ self.apk_path = os.path.join(out_directory, '{}.apk'.format(target_name))
+ if not os.path.exists(self.apk_path):
+ raise Exception("Can't find APK {}".format(self.apk_path))
+
+ # This flag is set when the main Run() loop exits. If Kill() is called
+ # after this flag is set, it will not do anything.
+ self.killed = threading.Event()
+
+ # Keep track of the port used by ADB forward in order to remove it later
+ # on.
+ self.local_port = None
+
+ def _IsValidIPv4Address(self, address):
+ """Returns True if address is a valid IPv4 address, False otherwise."""
+ try:
+ # inet_aton throws an exception if the address is not a valid IPv4
+ # address. However addresses such as '127.1' might still be considered
+ # valid, hence the check for 3 '.' in the address.
+ if socket.inet_aton(address) and address.count('.') == 3:
+ return True
+ except:
+ pass
+ return False
+
+ def _GetAdbDevices(self):
+ """Returns a list of names of connected devices, or empty list if none."""
+
+ # Does not use the ADBCommandBuilder class because this command should be
+ # run without targeting a specific device.
+ p = subprocess.Popen([_ADB, 'devices'], stderr=_DEV_NULL,
+ stdout=subprocess.PIPE, close_fds=True)
+ result = p.stdout.readlines()[1:-1]
+ p.wait()
+
+ names = []
+ for device in result:
+ name_info = device.split('\t')
+ # Some devices may not have authorization for USB debugging.
+ try:
+ if 'unauthorized' not in name_info[1]:
+ names.append(name_info[0])
+ # Sometimes happens when device is found, even though none are connected.
+ except IndexError:
+ continue
+ return names
+
+ def _IdentifyDevice(self):
+ """Picks a device to be used to run the executable.
+
+ In the event that no device_id is provided, but multiple
+ devices are connected, this method chooses the first device
+ listed.
+
+ Returns:
+ The name of an attached device, or None if no devices are present.
+ """
+ device_name = None
+
+ devices = self._GetAdbDevices()
+ if devices:
+ device_name = devices[0]
+
+ return device_name
+
+ def _ConnectIfNecessary(self):
+ """Run ADB connect if needed for devices connected over IP."""
+ if not self._IsValidIPv4Address(self.device_id):
+ return
+ for device in self._GetAdbDevices():
+ # Devices returned by _GetAdbDevices might include port number, so cannot
+ # simply check if self.device_id is in the returned list.
+ if self.device_id in device:
+ return
+
+ # Device isn't connected. Run ADB connect.
+ # Does not use the ADBCommandBuilder class because this command should be
+ # run without targeting a specific device.
+ p = subprocess.Popen([_ADB, 'connect', self.device_id], stderr=_DEV_NULL,
+ stdout=subprocess.PIPE, close_fds=True)
+ result = p.stdout.readlines()[0]
+ p.wait()
+
+ if 'connected to' not in result:
+ sys.stderr.write('Failed to connect to {}\n'.format(self.device_id))
+
+ def _LaunchCrowIfNecessary(self):
+ if self.device_id:
+ return
+
+ # Note that we just leave Crow running, since we uninstall/reinstall
+ # each time anyway.
+ self._CheckCall(*_CROW_COMMANDLINE)
+
+ def _Call(self, *args):
+ sys.stderr.write('{}\n'.format(' '.join(args)))
+ subprocess.call(args, stdout=_DEV_NULL, stderr=_DEV_NULL,
+ close_fds=True)
+
+ def _CallAdb(self, *in_args):
+ args = self.adb_builder.Build(*in_args)
+ self._Call(*args)
+
+ def _CheckCall(self, *args):
+ sys.stderr.write('{}\n'.format(' '.join(args)))
+ subprocess.check_call(args, stdout=_DEV_NULL, stderr=_DEV_NULL,
+ close_fds=True)
+
+ def _CheckCallAdb(self, *in_args):
+ args = self.adb_builder.Build(*in_args)
+ self._CheckCall(*args)
+
+ def _PopenAdb(self, *args, **kwargs):
+ return subprocess.Popen(self.adb_builder.Build(*args), close_fds=True,
+ **kwargs)
+
+ def Run(self):
+ # The return code for binaries run on Android is read from a log line that
+ # it emitted in android_main.cc. This return_code variable will be assigned
+ # the value read when we see that line, or left at 1 in the event of a crash
+ # or early exit.
+ return_code = 1
+
+ # Setup for running executable
+ self._LaunchCrowIfNecessary()
+ self._CheckCallAdb('wait-for-device')
+ self._Shutdown()
+
+ # Clear logcat
+ self._CheckCallAdb('logcat', '-c')
+
+ # Install the APK.
+ install_timer = StepTimer('install')
+ self._CheckCallAdb('install', '-r', self.apk_path)
+ install_timer.Stop()
+
+ # Send the wakeup key to ensure daydream isn't running, otherwise Activity
+ # Manager may get in a loop running the test over and over again.
+ self._CheckCallAdb('shell', 'input', 'keyevent', 'KEY_WAKEUP')
+
+ # Grant runtime permissions to avoid prompts during testing.
+ for permission in _RUNTIME_PERMISSIONS:
+ self._CheckCallAdb('shell', 'pm', 'grant', _APP_PACKAGE_NAME, permission)
+
+ done_queue = Queue.Queue()
+ am_monitor = AdbAmMonitorWatcher(self.adb_builder, done_queue)
+
+ # Increases the size of the logcat buffer. Without this, the log buffer
+ # will not flush quickly enough and output will be cut off.
+ self._CheckCallAdb('logcat', '-G', '2M')
+
+ # Ctrl + C will kill this process
+ logcat_process = self._PopenAdb(
+ 'logcat', '-v', 'raw', '-s', '*:F', 'DEBUG:*', 'System.err:*',
+ 'starboard:*', 'starboard_media:*',
+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+
+ # Actually running executable
+ run_timer = StepTimer('running executable')
+ try:
+ args = ['shell', 'am', 'start']
+ command_line_params = [
+ '--android_log_sleep_time=1000',
+ '--disable_sign_in',
+ ]
+ if self.target_command_line_params:
+ command_line_params += self.target_command_line_params
+ args += ['--esa', 'args', "'{}'".format(','.join(command_line_params))]
+ args += [_APP_START_INTENT]
+
+ self._CheckCallAdb(*args)
+
+ app_crashed = False
+ while True:
+ if not done_queue.empty():
+ done_queue_code = done_queue.get_nowait()
+ if done_queue_code == _QUEUE_CODE_CRASHED:
+ app_crashed = True
+ threading.Timer(_CRASH_LOG_SECONDS, logcat_process.kill).start()
+
+ # Note we cannot use "for line in logcat_process.stdout" because
+ # that uses a large buffer which will cause us to deadlock.
+ line = CleanLine(logcat_process.stdout.readline())
+
+ # Some crashes are not caught by the am_monitor thread, but they do
+ # produce the following string in logcat before they exit.
+ if 'beginning of crash' in line:
+ app_crashed = True
+ threading.Timer(_CRASH_LOG_SECONDS, logcat_process.kill).start()
+
+ if not line: # Logcat exited, or was killed
+ break
+ else:
+ self._WriteLine(line)
+ # Don't break until we see the below text in logcat, which should be
+ # written when the Starboard application event loop finishes.
+ if '***Application Stopped***' in line:
+ try:
+ return_code = int(line.split(' ')[-1])
+ except ValueError: # Error message was printed to stdout
+ pass
+ logcat_process.kill()
+ break
+
+ finally:
+ if app_crashed:
+ self._WriteLine('***Application Crashed***\n')
+ else:
+ self._Shutdown()
+ if self.local_port is not None:
+ self._CallAdb('forward', '--remove', 'tcp:{}'.format(self.local_port))
+ am_monitor.Shutdown()
+ self.killed.set()
+ run_timer.Stop()
+
+ return return_code
+
+ def _Shutdown(self):
+ self._CallAdb('shell', 'am', 'force-stop', _APP_PACKAGE_NAME)
+
+ def Kill(self):
+ if not self.killed.is_set():
+ sys.stderr.write('***Killing Launcher***\n')
+ self._CheckCallAdb('shell', 'log', '-t', 'starboard',
+ '***Application Stopped*** 1')
+ self._Shutdown()
+ else:
+ sys.stderr.write('Cannot kill launcher: already dead.\n')
+
+ def _WriteLine(self, line):
+ """Write log output to stdout."""
+ self.output_file.write(line)
+ self.output_file.flush()
+
+ def GetHostAndPortGivenPort(self, port):
+ forward_p = self._PopenAdb(
+ 'forward', 'tcp:0', 'tcp:{}'.format(port),
+ stdout=subprocess.PIPE,
+ stderr=_DEV_NULL)
+ forward_p.wait()
+
+ self.local_port = CleanLine(forward_p.stdout.readline()).rstrip('\n')
+ sys.stderr.write('ADB forward local port {} '
+ '=> device port {}\n'.format(self.local_port, port))
+ return socket.gethostbyname('localhost'), self.local_port
diff --git a/src/starboard/android/shared/log.cc b/src/starboard/android/shared/log.cc
new file mode 100644
index 0000000..a202f71
--- /dev/null
+++ b/src/starboard/android/shared/log.cc
@@ -0,0 +1,103 @@
+// Copyright 2015 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <android/log.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include <jni.h>
+#include <string>
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/android/shared/log_internal.h"
+#include "starboard/log.h"
+#include "starboard/shared/starboard/command_line.h"
+#include "starboard/string.h"
+#include "starboard/thread.h"
+
+using starboard::android::shared::JniEnvExt;
+using starboard::android::shared::ScopedLocalJavaRef;
+
+namespace {
+ const char kLogSleepTimeSwitch[] = "android_log_sleep_time";
+ SbTime g_log_sleep_time = 0;
+}
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+void LogInit(const starboard::shared::starboard::CommandLine& command_line) {
+ if (command_line.HasSwitch(kLogSleepTimeSwitch)) {
+ g_log_sleep_time =
+ SbStringAToL(command_line.GetSwitchValue(kLogSleepTimeSwitch).c_str());
+ SB_LOG(INFO) << "Android log sleep time: " << g_log_sleep_time;
+ }
+}
+
+} // namespace shared
+} // namespace android
+} // namespace starboard
+
+void SbLog(SbLogPriority priority, const char* message) {
+ int android_priority;
+ switch (priority) {
+ case kSbLogPriorityUnknown:
+ android_priority = ANDROID_LOG_UNKNOWN;
+ break;
+ case kSbLogPriorityInfo:
+ android_priority = ANDROID_LOG_INFO;
+ break;
+ case kSbLogPriorityWarning:
+ android_priority = ANDROID_LOG_WARN;
+ break;
+ case kSbLogPriorityError:
+ android_priority = ANDROID_LOG_ERROR;
+ break;
+ case kSbLogPriorityFatal:
+ android_priority = ANDROID_LOG_FATAL;
+ break;
+ default:
+ android_priority = ANDROID_LOG_INFO;
+ break;
+ }
+ __android_log_write(android_priority, "starboard", message);
+
+ // In unit tests the logging is too fast for the android log to be read out
+ // and we end up losing crucial logs. The test runner specifies a sleep time.
+ SbThreadSleep(g_log_sleep_time);
+}
+
+// Helper to write messages to logcat even when Android non-warning/non-error
+// logging is stripped from the app with Proguard.
+extern "C" SB_EXPORT_PLATFORM jint
+Java_dev_cobalt_util_Log_nativeWrite(JniEnvExt* env,
+ jobject unused_clazz,
+ jchar priority,
+ jstring tag,
+ jstring msg,
+ jobject throwable) {
+ char log_method_name[2] = {static_cast<char>(priority), '\0'};
+ if (throwable == nullptr) {
+ return env->CallStaticIntMethodOrAbort(
+ "android.util.Log", log_method_name,
+ "(Ljava/lang/String;Ljava/lang/String;)I", tag, msg);
+ } else {
+ return env->CallStaticIntMethodOrAbort(
+ "android.util.Log", log_method_name,
+ "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;)I",
+ tag, msg, throwable);
+ }
+}
diff --git a/src/starboard/android/shared/log_flush.cc b/src/starboard/android/shared/log_flush.cc
new file mode 100644
index 0000000..09d54e2
--- /dev/null
+++ b/src/starboard/android/shared/log_flush.cc
@@ -0,0 +1,21 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/log.h"
+
+#include <stdio.h>
+#include <unistd.h>
+
+void SbLogFlush() {
+}
diff --git a/src/starboard/android/shared/log_format.cc b/src/starboard/android/shared/log_format.cc
new file mode 100644
index 0000000..734d22f
--- /dev/null
+++ b/src/starboard/android/shared/log_format.cc
@@ -0,0 +1,54 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <stdio.h>
+#include <string>
+
+#include "starboard/log.h"
+#include "starboard/mutex.h"
+#include "starboard/string.h"
+
+namespace {
+SbMutex log_line_mutex = SB_MUTEX_INITIALIZER;
+std::stringstream log_line;
+const int kFormatBufferSizeBytes = 16 * 1024;
+
+} // namespace
+
+void SbLogFormat(const char* format, va_list arguments) {
+ // __android_log_vprint() cannot be used here because each call to it
+ // will produce a new line in the log, whereas a call to a function in
+ // the C vprintf family only produces a new line if fed "\n", etc. The logic
+ // below ensures that a log line is written only when a newline is detected,
+ // making Android SbLogFormat() behavior consistent with the expectations of
+ // the code that uses it, such as unit test suites.
+
+ char formatted_buffer[kFormatBufferSizeBytes];
+ vsprintf(formatted_buffer, format, arguments);
+
+ const char* newline = SbStringFindCharacter(formatted_buffer, '\n');
+
+ SbMutexAcquire(&log_line_mutex);
+ std::string buffer_string(formatted_buffer);
+ log_line << buffer_string;
+ if (newline != NULL) {
+ log_line.flush();
+
+ SbLogRaw(log_line.str().c_str());
+
+ log_line.str("");
+ log_line.clear();
+ }
+ SbMutexRelease(&log_line_mutex);
+}
diff --git a/src/starboard/android/shared/log_internal.h b/src/starboard/android/shared/log_internal.h
new file mode 100644
index 0000000..5fb5169
--- /dev/null
+++ b/src/starboard/android/shared/log_internal.h
@@ -0,0 +1,33 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// This header provides a mechanism for multiple Android logging
+// formats to share a single log file handle.
+
+#ifndef STARBOARD_ANDROID_SHARED_LOG_INTERNAL_H_
+#define STARBOARD_ANDROID_SHARED_LOG_INTERNAL_H_
+
+#include "starboard/shared/starboard/command_line.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+void LogInit(const starboard::shared::starboard::CommandLine& command_line);
+
+} // namespace shared
+} // namespace android
+} // namespace starboard
+
+#endif // STARBOARD_ANDROID_SHARED_LOG_INTERNAL_H_
diff --git a/src/starboard/android/shared/log_is_tty.cc b/src/starboard/android/shared/log_is_tty.cc
new file mode 100644
index 0000000..fba57de
--- /dev/null
+++ b/src/starboard/android/shared/log_is_tty.cc
@@ -0,0 +1,21 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/log.h"
+
+#include <unistd.h>
+
+bool SbLogIsTty() {
+ return false;
+}
diff --git a/src/starboard/android/shared/log_raw.cc b/src/starboard/android/shared/log_raw.cc
new file mode 100644
index 0000000..79d63ad
--- /dev/null
+++ b/src/starboard/android/shared/log_raw.cc
@@ -0,0 +1,19 @@
+// Copyright 2015 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/log.h"
+
+void SbLogRaw(const char* message) {
+ SbLog(kSbLogPriorityInfo, message);
+}
diff --git a/src/starboard/android/shared/main.cc b/src/starboard/android/shared/main.cc
new file mode 100644
index 0000000..5769390
--- /dev/null
+++ b/src/starboard/android/shared/main.cc
@@ -0,0 +1,23 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/export.h"
+#include "starboard/log.h"
+
+extern "C" SB_EXPORT_PLATFORM int main(int argc, char** argv) {
+ // main() is never called on Android. However, the cobalt_bin
+ // target requires it to be there.
+ SB_NOTREACHED();
+ return 0;
+}
diff --git a/src/starboard/android/shared/media_codec_bridge.cc b/src/starboard/android/shared/media_codec_bridge.cc
new file mode 100644
index 0000000..4ac70c0
--- /dev/null
+++ b/src/starboard/android/shared/media_codec_bridge.cc
@@ -0,0 +1,358 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/media_codec_bridge.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+namespace {
+
+// See
+// https://developer.android.com/reference/android/media/MediaFormat.html#COLOR_RANGE_FULL.
+const jint COLOR_RANGE_FULL = 1;
+const jint COLOR_RANGE_LIMITED = 2;
+
+const jint COLOR_STANDARD_BT2020 = 6;
+const jint COLOR_STANDARD_BT601_NTSC = 4;
+const jint COLOR_STANDARD_BT601_PAL = 2;
+const jint COLOR_STANDARD_BT709 = 1;
+
+const jint COLOR_TRANSFER_HLG = 7;
+const jint COLOR_TRANSFER_LINEAR = 1;
+const jint COLOR_TRANSFER_SDR_VIDEO = 3;
+const jint COLOR_TRANSFER_ST2084 = 6;
+
+// A special value to represent that no mapping between an SbMedia* HDR
+// metadata value and Android HDR metadata value is possible. This value
+// implies that HDR playback should not be attempted.
+const jint COLOR_VALUE_UNKNOWN = -1;
+
+jint SbMediaPrimaryIdToColorStandard(SbMediaPrimaryId primary_id) {
+ switch (primary_id) {
+ case kSbMediaPrimaryIdBt709:
+ return COLOR_STANDARD_BT709;
+ case kSbMediaPrimaryIdBt2020:
+ return COLOR_STANDARD_BT2020;
+ default:
+ return COLOR_VALUE_UNKNOWN;
+ }
+}
+
+jint SbMediaTransferIdToColorTransfer(SbMediaTransferId transfer_id) {
+ switch (transfer_id) {
+ case kSbMediaTransferIdBt709:
+ return COLOR_TRANSFER_SDR_VIDEO;
+ case kSbMediaTransferIdSmpteSt2084:
+ return COLOR_TRANSFER_ST2084;
+ case kSbMediaTransferIdAribStdB67:
+ return COLOR_TRANSFER_HLG;
+ default:
+ return COLOR_VALUE_UNKNOWN;
+ }
+}
+
+jint SbMediaRangeIdToColorRange(SbMediaRangeId range_id) {
+ switch (range_id) {
+ case kSbMediaRangeIdLimited:
+ return COLOR_RANGE_LIMITED;
+ case kSbMediaRangeIdFull:
+ return COLOR_RANGE_FULL;
+ default:
+ return COLOR_VALUE_UNKNOWN;
+ }
+}
+
+} // namespace
+
+// static
+scoped_ptr<MediaCodecBridge> MediaCodecBridge::CreateAudioMediaCodecBridge(
+ SbMediaAudioCodec audio_codec,
+ const SbMediaAudioHeader& audio_header,
+ jobject j_media_crypto) {
+ const char* mime = SupportedAudioCodecToMimeType(audio_codec);
+ if (!mime) {
+ return scoped_ptr<MediaCodecBridge>(NULL);
+ }
+ JniEnvExt* env = JniEnvExt::Get();
+ ScopedLocalJavaRef<jstring> j_mime(env->NewStringStandardUTFOrAbort(mime));
+ jobject j_media_codec_bridge = env->CallStaticObjectMethodOrAbort(
+ "dev/cobalt/media/MediaCodecBridge", "createAudioMediaCodecBridge",
+ "(Ljava/lang/String;ZZIILandroid/media/MediaCrypto;)Ldev/cobalt/media/"
+ "MediaCodecBridge;",
+ j_mime.Get(), !!j_media_crypto, false, audio_header.samples_per_second,
+ audio_header.number_of_channels, j_media_crypto);
+
+ if (!j_media_codec_bridge) {
+ return scoped_ptr<MediaCodecBridge>(NULL);
+ }
+
+ j_media_codec_bridge = env->ConvertLocalRefToGlobalRef(j_media_codec_bridge);
+ return scoped_ptr<MediaCodecBridge>(
+ new MediaCodecBridge(j_media_codec_bridge));
+}
+
+// static
+scoped_ptr<MediaCodecBridge> MediaCodecBridge::CreateVideoMediaCodecBridge(
+ SbMediaVideoCodec video_codec,
+ int width,
+ int height,
+ jobject j_surface,
+ jobject j_media_crypto,
+ const SbMediaColorMetadata* color_metadata) {
+ const char* mime = SupportedVideoCodecToMimeType(video_codec);
+ if (!mime) {
+ return scoped_ptr<MediaCodecBridge>(NULL);
+ }
+ JniEnvExt* env = JniEnvExt::Get();
+ ScopedLocalJavaRef<jstring> j_mime(env->NewStringStandardUTFOrAbort(mime));
+
+ ScopedLocalJavaRef<jobject> j_color_info(nullptr);
+ if (color_metadata) {
+ jint color_standard =
+ SbMediaPrimaryIdToColorStandard(color_metadata->primaries);
+ jint color_transfer =
+ SbMediaTransferIdToColorTransfer(color_metadata->transfer);
+ jint color_range = SbMediaRangeIdToColorRange(color_metadata->range);
+
+ if (color_standard != COLOR_VALUE_UNKNOWN &&
+ color_transfer != COLOR_VALUE_UNKNOWN &&
+ color_range != COLOR_VALUE_UNKNOWN) {
+ const auto& mastering_metadata = color_metadata->mastering_metadata;
+ j_color_info.Reset(env->NewObjectOrAbort(
+ "dev/cobalt/media/MediaCodecBridge$ColorInfo", "(IIIFFFFFFFFFF)V",
+ color_range, color_standard, color_transfer,
+ mastering_metadata.primary_r_chromaticity_x,
+ mastering_metadata.primary_r_chromaticity_y,
+ mastering_metadata.primary_g_chromaticity_x,
+ mastering_metadata.primary_g_chromaticity_y,
+ mastering_metadata.primary_b_chromaticity_x,
+ mastering_metadata.primary_b_chromaticity_y,
+ mastering_metadata.white_point_chromaticity_x,
+ mastering_metadata.white_point_chromaticity_y,
+ mastering_metadata.luminance_max, mastering_metadata.luminance_min));
+ }
+ }
+
+ jobject j_media_codec_bridge = env->CallStaticObjectMethodOrAbort(
+ "dev/cobalt/media/MediaCodecBridge", "createVideoMediaCodecBridge",
+ "(Ljava/lang/String;ZZIILandroid/view/Surface;"
+ "Landroid/media/MediaCrypto;"
+ "Ldev/cobalt/media/MediaCodecBridge$ColorInfo;)"
+ "Ldev/cobalt/media/MediaCodecBridge;",
+ j_mime.Get(), !!j_media_crypto, false, width, height, j_surface,
+ j_media_crypto, j_color_info.Get());
+
+ if (!j_media_codec_bridge) {
+ return scoped_ptr<MediaCodecBridge>(NULL);
+ }
+
+ j_media_codec_bridge = env->ConvertLocalRefToGlobalRef(j_media_codec_bridge);
+ return scoped_ptr<MediaCodecBridge>(
+ new MediaCodecBridge(j_media_codec_bridge));
+}
+
+MediaCodecBridge::~MediaCodecBridge() {
+ JniEnvExt* env = JniEnvExt::Get();
+
+ SB_DCHECK(j_media_codec_bridge_);
+ env->CallVoidMethodOrAbort(j_media_codec_bridge_, "release", "()V");
+ env->DeleteGlobalRef(j_media_codec_bridge_);
+ j_media_codec_bridge_ = NULL;
+
+ SB_DCHECK(j_reused_dequeue_input_result_);
+ env->DeleteGlobalRef(j_reused_dequeue_input_result_);
+ j_reused_dequeue_input_result_ = NULL;
+
+ SB_DCHECK(j_reused_dequeue_output_result_);
+ env->DeleteGlobalRef(j_reused_dequeue_output_result_);
+ j_reused_dequeue_output_result_ = NULL;
+
+ SB_DCHECK(j_reused_get_output_format_result_);
+ env->DeleteGlobalRef(j_reused_get_output_format_result_);
+ j_reused_get_output_format_result_ = NULL;
+}
+
+DequeueInputResult MediaCodecBridge::DequeueInputBuffer(jlong timeout_us) {
+ JniEnvExt* env = JniEnvExt::Get();
+ env->CallVoidMethodOrAbort(
+ j_media_codec_bridge_, "dequeueInputBuffer",
+ "(JLdev/cobalt/media/MediaCodecBridge$DequeueInputResult;)V", timeout_us,
+ j_reused_dequeue_input_result_);
+ return {env->CallIntMethodOrAbort(j_reused_dequeue_input_result_, "status",
+ "()I"),
+ env->CallIntMethodOrAbort(j_reused_dequeue_input_result_, "index",
+ "()I")};
+}
+
+jobject MediaCodecBridge::GetInputBuffer(jint index) {
+ SB_DCHECK(index >= 0);
+ return JniEnvExt::Get()->CallObjectMethodOrAbort(
+ j_media_codec_bridge_, "getInputBuffer", "(I)Ljava/nio/ByteBuffer;",
+ index);
+}
+
+jint MediaCodecBridge::QueueInputBuffer(jint index,
+ jint offset,
+ jint size,
+ jlong presentation_time_microseconds,
+ jint flags) {
+ return JniEnvExt::Get()->CallIntMethodOrAbort(
+ j_media_codec_bridge_, "queueInputBuffer", "(IIIJI)I", index, offset,
+ size, presentation_time_microseconds, flags);
+}
+
+jint MediaCodecBridge::QueueSecureInputBuffer(
+ jint index,
+ jint offset,
+ const SbDrmSampleInfo& drm_sample_info,
+ jlong presentation_time_microseconds) {
+ JniEnvExt* env = JniEnvExt::Get();
+ ScopedLocalJavaRef<jbyteArray> j_iv(env->NewByteArrayFromRaw(
+ reinterpret_cast<const jbyte*>(drm_sample_info.initialization_vector),
+ drm_sample_info.initialization_vector_size));
+ ScopedLocalJavaRef<jbyteArray> j_key_id(env->NewByteArrayFromRaw(
+ reinterpret_cast<const jbyte*>(drm_sample_info.identifier),
+ drm_sample_info.identifier_size));
+
+ // Reshape the sub sample mapping like this:
+ // [(c0, e0), (c1, e1), ...] -> [c0, c1, ...] and [e0, e1, ...]
+ int32_t subsample_count = drm_sample_info.subsample_count;
+ scoped_array<jint> clear_bytes(new jint[subsample_count]);
+ scoped_array<jint> encrypted_bytes(new jint[subsample_count]);
+ for (int i = 0; i < subsample_count; ++i) {
+ clear_bytes[i] = drm_sample_info.subsample_mapping[i].clear_byte_count;
+ encrypted_bytes[i] =
+ drm_sample_info.subsample_mapping[i].encrypted_byte_count;
+ }
+ ScopedLocalJavaRef<jintArray> j_clear_bytes(
+ env->NewIntArrayFromRaw(clear_bytes.get(), subsample_count));
+ ScopedLocalJavaRef<jintArray> j_encrypted_bytes(
+ env->NewIntArrayFromRaw(encrypted_bytes.get(), subsample_count));
+
+ return env->CallIntMethodOrAbort(
+ j_media_codec_bridge_, "queueSecureInputBuffer", "(II[B[B[I[IIIIIJ)I",
+ index, offset, j_iv.Get(), j_key_id.Get(), j_clear_bytes.Get(),
+ j_encrypted_bytes.Get(), subsample_count, CRYPTO_MODE_AES_CTR, 0, 0,
+ presentation_time_microseconds);
+}
+
+DequeueOutputResult MediaCodecBridge::DequeueOutputBuffer(jlong timeout_us) {
+ JniEnvExt* env = JniEnvExt::Get();
+ env->CallVoidMethodOrAbort(
+ j_media_codec_bridge_, "dequeueOutputBuffer",
+ "(JLdev/cobalt/media/MediaCodecBridge$DequeueOutputResult;)V", timeout_us,
+ j_reused_dequeue_output_result_);
+ return {env->CallIntMethodOrAbort(j_reused_dequeue_output_result_, "status",
+ "()I"),
+ env->CallIntMethodOrAbort(j_reused_dequeue_output_result_, "index",
+ "()I"),
+ env->CallIntMethodOrAbort(j_reused_dequeue_output_result_, "flags",
+ "()I"),
+ env->CallIntMethodOrAbort(j_reused_dequeue_output_result_, "offset",
+ "()I"),
+ env->CallLongMethodOrAbort(j_reused_dequeue_output_result_,
+ "presentationTimeMicroseconds", "()J"),
+ env->CallIntMethodOrAbort(j_reused_dequeue_output_result_, "numBytes",
+ "()I")};
+}
+
+jobject MediaCodecBridge::GetOutputBuffer(jint index) {
+ SB_DCHECK(index >= 0);
+ return JniEnvExt::Get()->CallObjectMethodOrAbort(
+ j_media_codec_bridge_, "getOutputBuffer", "(I)Ljava/nio/ByteBuffer;",
+ index);
+}
+
+void MediaCodecBridge::ReleaseOutputBuffer(jint index, jboolean render) {
+ JniEnvExt::Get()->CallVoidMethodOrAbort(
+ j_media_codec_bridge_, "releaseOutputBuffer", "(IZ)V", index, render);
+}
+
+void MediaCodecBridge::ReleaseOutputBufferAtTimestamp(
+ jint index,
+ jlong render_timestamp_ns) {
+ JniEnvExt::Get()->CallVoidMethodOrAbort(j_media_codec_bridge_,
+ "releaseOutputBuffer", "(IJ)V", index,
+ render_timestamp_ns);
+}
+
+jint MediaCodecBridge::Flush() {
+ return JniEnvExt::Get()->CallIntMethodOrAbort(j_media_codec_bridge_, "flush",
+ "()I");
+}
+
+SurfaceDimensions MediaCodecBridge::GetOutputDimensions() {
+ JniEnvExt* env = JniEnvExt::Get();
+ env->CallVoidMethodOrAbort(
+ j_media_codec_bridge_, "getOutputFormat",
+ "(Ldev/cobalt/media/MediaCodecBridge$GetOutputFormatResult;)V",
+ j_reused_get_output_format_result_);
+ return {env->CallIntMethodOrAbort(j_reused_get_output_format_result_, "width",
+ "()I"),
+ env->CallIntMethodOrAbort(j_reused_get_output_format_result_,
+ "height", "()I")};
+}
+
+AudioOutputFormatResult MediaCodecBridge::GetAudioOutputFormat() {
+ JniEnvExt* env = JniEnvExt::Get();
+ env->CallVoidMethodOrAbort(
+ j_media_codec_bridge_, "getOutputFormat",
+ "(Ldev/cobalt/media/MediaCodecBridge$GetOutputFormatResult;)V",
+ j_reused_get_output_format_result_);
+
+ jint status = env->CallIntMethodOrAbort(j_reused_get_output_format_result_,
+ "status", "()I");
+ if (status == MEDIA_CODEC_ERROR) {
+ return {status, 0, 0};
+ }
+
+ return {status, env->CallIntMethodOrAbort(j_reused_get_output_format_result_,
+ "sampleRate", "()I"),
+ env->CallIntMethodOrAbort(j_reused_get_output_format_result_,
+ "channelCount", "()I")};
+}
+
+MediaCodecBridge::MediaCodecBridge(jobject j_media_codec_bridge)
+ : j_media_codec_bridge_(j_media_codec_bridge),
+ j_reused_dequeue_input_result_(NULL),
+ j_reused_dequeue_output_result_(NULL),
+ j_reused_get_output_format_result_(NULL) {
+ SB_DCHECK(j_media_codec_bridge_);
+ JniEnvExt* env = JniEnvExt::Get();
+ SB_DCHECK(env->GetObjectRefType(j_media_codec_bridge_) == JNIGlobalRefType);
+
+ j_reused_dequeue_input_result_ = env->NewObjectOrAbort(
+ "dev/cobalt/media/MediaCodecBridge$DequeueInputResult", "()V");
+ SB_DCHECK(j_reused_dequeue_input_result_);
+ j_reused_dequeue_input_result_ =
+ env->ConvertLocalRefToGlobalRef(j_reused_dequeue_input_result_);
+
+ j_reused_dequeue_output_result_ = env->NewObjectOrAbort(
+ "dev/cobalt/media/MediaCodecBridge$DequeueOutputResult", "()V");
+ SB_DCHECK(j_reused_dequeue_output_result_);
+ j_reused_dequeue_output_result_ =
+ env->ConvertLocalRefToGlobalRef(j_reused_dequeue_output_result_);
+
+ j_reused_get_output_format_result_ = env->NewObjectOrAbort(
+ "dev/cobalt/media/MediaCodecBridge$GetOutputFormatResult", "()V");
+ SB_DCHECK(j_reused_get_output_format_result_);
+ j_reused_get_output_format_result_ =
+ env->ConvertLocalRefToGlobalRef(j_reused_get_output_format_result_);
+}
+
+} // namespace shared
+} // namespace android
+} // namespace starboard
diff --git a/src/starboard/android/shared/media_codec_bridge.h b/src/starboard/android/shared/media_codec_bridge.h
new file mode 100644
index 0000000..3172e33
--- /dev/null
+++ b/src/starboard/android/shared/media_codec_bridge.h
@@ -0,0 +1,139 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_MEDIA_CODEC_BRIDGE_H_
+#define STARBOARD_ANDROID_SHARED_MEDIA_CODEC_BRIDGE_H_
+
+#include <string>
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/android/shared/media_common.h"
+#include "starboard/common/scoped_ptr.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+// These must be in sync with MediaCodecWrapper.MEDIA_CODEC_XXX constants in
+// MediaCodecBridge.java.
+const jint MEDIA_CODEC_OK = 0;
+const jint MEDIA_CODEC_DEQUEUE_INPUT_AGAIN_LATER = 1;
+const jint MEDIA_CODEC_DEQUEUE_OUTPUT_AGAIN_LATER = 2;
+const jint MEDIA_CODEC_OUTPUT_BUFFERS_CHANGED = 3;
+const jint MEDIA_CODEC_OUTPUT_FORMAT_CHANGED = 4;
+const jint MEDIA_CODEC_INPUT_END_OF_STREAM = 5;
+const jint MEDIA_CODEC_OUTPUT_END_OF_STREAM = 6;
+const jint MEDIA_CODEC_NO_KEY = 7;
+const jint MEDIA_CODEC_INSUFFICIENT_OUTPUT_PROTECTION = 8;
+const jint MEDIA_CODEC_ABORT = 9;
+const jint MEDIA_CODEC_ERROR = 10;
+
+const jint BUFFER_FLAG_CODEC_CONFIG = 2;
+const jint BUFFER_FLAG_END_OF_STREAM = 4;
+
+const jint CRYPTO_MODE_UNENCRYPTED = 0;
+const jint CRYPTO_MODE_AES_CTR = 1;
+const jint CRYPTO_MODE_AES_CBC = 2;
+
+struct DequeueInputResult {
+ jint status;
+ jint index;
+};
+
+struct DequeueOutputResult {
+ jint status;
+ jint index;
+ jint flags;
+ jint offset;
+ jlong presentation_time_microseconds;
+ jint num_bytes;
+};
+
+struct SurfaceDimensions {
+ jint width;
+ jint height;
+};
+
+struct AudioOutputFormatResult {
+ jint status;
+ jint sample_rate;
+ jint channel_count;
+};
+
+class MediaCodecBridge {
+ public:
+ static scoped_ptr<MediaCodecBridge> CreateAudioMediaCodecBridge(
+ SbMediaAudioCodec audio_codec,
+ const SbMediaAudioHeader& audio_header,
+ jobject j_media_crypto);
+
+ static scoped_ptr<MediaCodecBridge> CreateVideoMediaCodecBridge(
+ SbMediaVideoCodec video_codec,
+ int width,
+ int height,
+ jobject j_surface,
+ jobject j_media_crypto,
+ const SbMediaColorMetadata* color_metadata);
+
+ ~MediaCodecBridge();
+
+ DequeueInputResult DequeueInputBuffer(jlong timeout_us);
+ // It is the responsibility of the client to manage the lifetime of the
+ // jobject that |GetInputBuffer| returns.
+ jobject GetInputBuffer(jint index);
+ jint QueueInputBuffer(jint index,
+ jint offset,
+ jint size,
+ jlong presentation_time_microseconds,
+ jint flags);
+ jint QueueSecureInputBuffer(jint index,
+ jint offset,
+ const SbDrmSampleInfo& drm_sample_info,
+ jlong presentation_time_microseconds);
+
+ DequeueOutputResult DequeueOutputBuffer(jlong timeout_us);
+ // It is the responsibility of the client to manage the lifetime of the
+ // jobject that |GetOutputBuffer| returns.
+ jobject GetOutputBuffer(jint index);
+ void ReleaseOutputBuffer(jint index, jboolean render);
+ void ReleaseOutputBufferAtTimestamp(jint index, jlong render_timestamp_ns);
+
+ jint Flush();
+ SurfaceDimensions GetOutputDimensions();
+ AudioOutputFormatResult GetAudioOutputFormat();
+
+ private:
+ // |MediaCodecBridge|s must only be created through its factory methods.
+ explicit MediaCodecBridge(jobject j_media_codec_bridge);
+
+ jobject j_media_codec_bridge_;
+
+ // Profiling and allocation tracking has identified this area to be hot,
+ // and, capable of enough to cause GC times to raise high enough to impact
+ // playback. We mitigate this by reusing these output objects between calls
+ // to |DequeueInputBuffer|, |DequeueOutputBuffer|, and
+ // |GetOutputDimensions|.
+ jobject j_reused_dequeue_input_result_;
+ jobject j_reused_dequeue_output_result_;
+ jobject j_reused_get_output_format_result_;
+
+ SB_DISALLOW_COPY_AND_ASSIGN(MediaCodecBridge);
+};
+
+} // namespace shared
+} // namespace android
+} // namespace starboard
+
+#endif // STARBOARD_ANDROID_SHARED_MEDIA_CODEC_BRIDGE_H_
diff --git a/src/starboard/android/shared/media_common.h b/src/starboard/android/shared/media_common.h
new file mode 100644
index 0000000..78ba783
--- /dev/null
+++ b/src/starboard/android/shared/media_common.h
@@ -0,0 +1,102 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_MEDIA_COMMON_H_
+#define STARBOARD_ANDROID_SHARED_MEDIA_COMMON_H_
+
+#include <deque>
+#include <queue>
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/configuration.h"
+#include "starboard/log.h"
+#include "starboard/media.h"
+#include "starboard/mutex.h"
+#include "starboard/shared/starboard/player/filter/audio_frame_tracker.h"
+#include "starboard/string.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+const int64_t kSecondInMicroseconds = 1000 * 1000;
+
+inline bool IsWidevine(const char* key_system) {
+ return SbStringCompareAll(key_system, "com.widevine") == 0 ||
+ SbStringCompareAll(key_system, "com.widevine.alpha") == 0;
+}
+
+// Map a supported |SbMediaAudioCodec| into its corresponding mime type
+// string. Returns |NULL| if |audio_codec| is not supported.
+inline const char* SupportedAudioCodecToMimeType(
+ const SbMediaAudioCodec audio_codec) {
+ if (audio_codec == kSbMediaAudioCodecAac) {
+ return "audio/mp4a-latm";
+ }
+ return NULL;
+}
+
+// Map a supported |SbMediaVideoCodec| into its corresponding mime type
+// string. Returns |NULL| if |video_codec| is not supported.
+inline const char* SupportedVideoCodecToMimeType(
+ const SbMediaVideoCodec video_codec) {
+ if (video_codec == kSbMediaVideoCodecVp9) {
+ return "video/x-vnd.on2.vp9";
+ } else if (video_codec == kSbMediaVideoCodecH264) {
+ return "video/avc";
+ }
+ return NULL;
+}
+
+// A simple thread-safe queue for events of type |E|, that supports polling
+// based access only.
+template <typename E>
+class EventQueue {
+ public:
+ E PollFront() {
+ ScopedLock lock(mutex_);
+ if (!deque_.empty()) {
+ E event = deque_.front();
+ deque_.pop_front();
+ return event;
+ }
+
+ return E();
+ }
+
+ void PushBack(const E& event) {
+ ScopedLock lock(mutex_);
+ deque_.push_back(event);
+ }
+
+ void Clear() {
+ ScopedLock lock(mutex_);
+ deque_.clear();
+ }
+
+ size_t size() const {
+ ScopedLock lock(mutex_);
+ return deque_.size();
+ }
+
+ private:
+ ::starboard::Mutex mutex_;
+ std::deque<E> deque_;
+};
+
+} // namespace shared
+} // namespace android
+} // namespace starboard
+
+#endif // STARBOARD_ANDROID_SHARED_MEDIA_COMMON_H_
diff --git a/src/starboard/android/shared/media_decoder.cc b/src/starboard/android/shared/media_decoder.cc
new file mode 100644
index 0000000..507239e
--- /dev/null
+++ b/src/starboard/android/shared/media_decoder.cc
@@ -0,0 +1,393 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/media_decoder.h"
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/android/shared/media_common.h"
+#include "starboard/audio_sink.h"
+#include "starboard/log.h"
+#include "starboard/string.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+namespace {
+
+const jlong kDequeueTimeout = 0;
+
+const jint kNoOffset = 0;
+const jlong kNoPts = 0;
+const jint kNoSize = 0;
+const jint kNoBufferFlags = 0;
+
+const char* GetNameForMediaCodecStatus(jint status) {
+ switch (status) {
+ case MEDIA_CODEC_OK:
+ return "MEDIA_CODEC_OK";
+ case MEDIA_CODEC_DEQUEUE_INPUT_AGAIN_LATER:
+ return "MEDIA_CODEC_DEQUEUE_INPUT_AGAIN_LATER";
+ case MEDIA_CODEC_DEQUEUE_OUTPUT_AGAIN_LATER:
+ return "MEDIA_CODEC_DEQUEUE_OUTPUT_AGAIN_LATER";
+ case MEDIA_CODEC_OUTPUT_BUFFERS_CHANGED:
+ return "MEDIA_CODEC_OUTPUT_BUFFERS_CHANGED";
+ case MEDIA_CODEC_OUTPUT_FORMAT_CHANGED:
+ return "MEDIA_CODEC_OUTPUT_FORMAT_CHANGED";
+ case MEDIA_CODEC_INPUT_END_OF_STREAM:
+ return "MEDIA_CODEC_INPUT_END_OF_STREAM";
+ case MEDIA_CODEC_OUTPUT_END_OF_STREAM:
+ return "MEDIA_CODEC_OUTPUT_END_OF_STREAM";
+ case MEDIA_CODEC_NO_KEY:
+ return "MEDIA_CODEC_NO_KEY";
+ case MEDIA_CODEC_INSUFFICIENT_OUTPUT_PROTECTION:
+ return "MEDIA_CODEC_INSUFFICIENT_OUTPUT_PROTECTION";
+ case MEDIA_CODEC_ABORT:
+ return "MEDIA_CODEC_ABORT";
+ case MEDIA_CODEC_ERROR:
+ return "MEDIA_CODEC_ERROR";
+ default:
+ SB_NOTREACHED();
+ return "MEDIA_CODEC_ERROR_UNKNOWN";
+ }
+}
+
+} // namespace
+
+MediaDecoder::MediaDecoder(Host* host,
+ SbMediaAudioCodec audio_codec,
+ const SbMediaAudioHeader& audio_header,
+ SbDrmSystem drm_system)
+ : media_type_(kSbMediaTypeAudio),
+ host_(host),
+ decoder_thread_(kSbThreadInvalid),
+ media_codec_bridge_(NULL),
+ stream_ended_(false),
+ drm_system_(static_cast<DrmSystem*>(drm_system)) {
+ SB_DCHECK(host_);
+
+ jobject j_media_crypto = drm_system_ ? drm_system_->GetMediaCrypto() : NULL;
+ SB_DCHECK(!drm_system_ || j_media_crypto);
+ media_codec_bridge_ = MediaCodecBridge::CreateAudioMediaCodecBridge(
+ audio_codec, audio_header, j_media_crypto);
+ if (!media_codec_bridge_) {
+ SB_LOG(ERROR) << "Failed to create audio media codec bridge.";
+ return;
+ }
+ if (audio_header.audio_specific_config_size > 0) {
+ // |audio_header.audio_specific_config| is guaranteed to be outlived the
+ // decoder as it is stored in |FilterBasedPlayerWorkerHandler|.
+ event_queue_.PushBack(Event(
+ static_cast<const int8_t*>(audio_header.audio_specific_config),
+ audio_header.audio_specific_config_size));
+ }
+}
+
+MediaDecoder::MediaDecoder(Host* host,
+ SbMediaVideoCodec video_codec,
+ int width,
+ int height,
+ jobject j_output_surface,
+ SbDrmSystem drm_system,
+ const SbMediaColorMetadata* color_metadata)
+ : media_type_(kSbMediaTypeVideo),
+ host_(host),
+ stream_ended_(false),
+ drm_system_(static_cast<DrmSystem*>(drm_system)),
+ decoder_thread_(kSbThreadInvalid),
+ media_codec_bridge_(NULL) {
+ jobject j_media_crypto = drm_system_ ? drm_system_->GetMediaCrypto() : NULL;
+ SB_DCHECK(!drm_system_ || j_media_crypto);
+ media_codec_bridge_ = MediaCodecBridge::CreateVideoMediaCodecBridge(
+ video_codec, width, height, j_output_surface, j_media_crypto,
+ color_metadata);
+ if (!media_codec_bridge_) {
+ SB_LOG(ERROR) << "Failed to create video media codec bridge.";
+ }
+}
+
+MediaDecoder::~MediaDecoder() {
+ SB_DCHECK(thread_checker_.CalledOnValidThread());
+
+ JoinOnDecoderThread();
+}
+
+void MediaDecoder::Initialize(const ErrorCB& error_cb) {
+ SB_DCHECK(thread_checker_.CalledOnValidThread());
+ SB_DCHECK(error_cb);
+ SB_DCHECK(!error_cb_);
+
+ error_cb_ = error_cb;
+}
+
+void MediaDecoder::WriteInputBuffer(
+ const scoped_refptr<InputBuffer>& input_buffer) {
+ SB_DCHECK(thread_checker_.CalledOnValidThread());
+ SB_DCHECK(input_buffer);
+
+ if (stream_ended_) {
+ SB_LOG(ERROR) << "Decode() is called after WriteEndOfStream() is called.";
+ return;
+ }
+
+ if (!SbThreadIsValid(decoder_thread_)) {
+ decoder_thread_ = SbThreadCreate(
+ 0, kSbThreadPriorityNormal, kSbThreadNoAffinity, true,
+ media_type_ == kSbMediaTypeAudio ? "audio_decoder" : "video_decoder",
+ &MediaDecoder::ThreadEntryPoint, this);
+ SB_DCHECK(SbThreadIsValid(decoder_thread_));
+ }
+
+ event_queue_.PushBack(Event(input_buffer));
+}
+
+void MediaDecoder::WriteEndOfStream() {
+ SB_DCHECK(thread_checker_.CalledOnValidThread());
+
+ stream_ended_ = true;
+ event_queue_.PushBack(Event(Event::kWriteEndOfStream));
+}
+
+// static
+void* MediaDecoder::ThreadEntryPoint(void* context) {
+ SB_DCHECK(context);
+ MediaDecoder* decoder = static_cast<MediaDecoder*>(context);
+ decoder->DecoderThreadFunc();
+ return NULL;
+}
+
+void MediaDecoder::DecoderThreadFunc() {
+ SB_DCHECK(error_cb_);
+
+ // TODO: Replace |pending_work| with a single object instead of using a deque.
+ std::deque<Event> pending_work;
+
+ // TODO: Refactor the i/o logic using async based decoder.
+ while (!destroying_.load()) {
+ if (pending_work.empty()) {
+ Event event = event_queue_.PollFront();
+
+ if (event.type == Event::kWriteInputBuffer ||
+ event.type == Event::kWriteEndOfStream ||
+ event.type == Event::kWriteCodecConfig) {
+ pending_work.push_back(event);
+ }
+ }
+
+ if (media_type_ == kSbMediaTypeAudio) {
+ if (!ProcessOneInputBuffer(&pending_work) &&
+ !DequeueAndProcessOutputBuffer()) {
+ SbThreadSleep(kSbTimeMillisecond);
+ }
+ continue;
+ }
+
+ SB_DCHECK(media_type_ == kSbMediaTypeVideo);
+ // Call Tick() to give the video decoder a chance to release the frames
+ // after each input or output operations.
+ if (!ProcessOneInputBuffer(&pending_work) &&
+ !host_->Tick(media_codec_bridge_.get())) {
+ SbThreadSleep(kSbTimeMillisecond);
+ }
+ if (!DequeueAndProcessOutputBuffer() &&
+ !host_->Tick(media_codec_bridge_.get())) {
+ SbThreadSleep(kSbTimeMillisecond);
+ }
+ }
+
+ SB_LOG(INFO) << "Destroying decoder thread.";
+ host_->OnFlushing();
+ jint status = media_codec_bridge_->Flush();
+ if (status != MEDIA_CODEC_OK) {
+ SB_LOG(ERROR) << "Failed to flush media codec.";
+ }
+}
+
+void MediaDecoder::JoinOnDecoderThread() {
+ if (!SbThreadIsValid(decoder_thread_)) {
+ return;
+ }
+ destroying_.store(true);
+ SbThreadJoin(decoder_thread_, NULL);
+ event_queue_.Clear();
+ decoder_thread_ = kSbThreadInvalid;
+}
+
+bool MediaDecoder::ProcessOneInputBuffer(std::deque<Event>* pending_work) {
+ SB_DCHECK(pending_work);
+ if (pending_work->empty()) {
+ return false;
+ }
+
+ SB_CHECK(media_codec_bridge_);
+
+ // During secure playback, and only secure playback, is is possible that our
+ // attempt to enqueue an input buffer will be rejected by MediaCodec because
+ // we do not have a key yet. In this case, we hold on to the input buffer
+ // that we have already set up, and repeatedly attempt to enqueue it until
+ // it works. Ideally, we would just wait until MediaDrm was ready, however
+ // the shared starboard player framework assumes that it is possible to
+ // perform decryption and decoding as separate steps, so from its
+ // perspective, having made it to this point implies that we ready to
+ // decode. It is not possible to do them as separate steps on Android. From
+ // the perspective of user application, decryption and decoding are one
+ // atomic step.
+ DequeueInputResult dequeue_input_result;
+ Event event;
+ bool input_buffer_already_written = false;
+ if (pending_queue_input_buffer_task_) {
+ dequeue_input_result =
+ pending_queue_input_buffer_task_->dequeue_input_result;
+ SB_DCHECK(dequeue_input_result.index >= 0);
+ event = pending_queue_input_buffer_task_->event;
+ pending_queue_input_buffer_task_ = nullopt_t();
+ input_buffer_already_written = true;
+ } else {
+ dequeue_input_result =
+ media_codec_bridge_->DequeueInputBuffer(kDequeueTimeout);
+ event = pending_work->front();
+ if (dequeue_input_result.index < 0) {
+ HandleError("dequeueInputBuffer", dequeue_input_result.status);
+ return false;
+ }
+ pending_work->pop_front();
+ }
+
+ SB_DCHECK(event.type == Event::kWriteCodecConfig ||
+ event.type == Event::kWriteInputBuffer ||
+ event.type == Event::kWriteEndOfStream);
+ const scoped_refptr<InputBuffer>& input_buffer = event.input_buffer;
+ if (event.type == Event::kWriteEndOfStream) {
+ SB_DCHECK(pending_work->empty());
+ }
+ const void* data = NULL;
+ int size = 0;
+ if (event.type == Event::kWriteCodecConfig) {
+ SB_DCHECK(media_type_ == kSbMediaTypeAudio);
+ data = event.codec_config;
+ size = event.codec_config_size;
+ } else if (event.type == Event::kWriteInputBuffer) {
+ data = input_buffer->data();
+ size = input_buffer->size();
+ } else if (event.type == Event::kWriteEndOfStream) {
+ data = NULL;
+ size = 0;
+ }
+
+ // Don't bother rewriting the same data if we already did it last time we
+ // were called and had it stored in |pending_queue_input_buffer_task_|.
+ if (!input_buffer_already_written && event.type != Event::kWriteEndOfStream) {
+ ScopedJavaByteBuffer byte_buffer(
+ media_codec_bridge_->GetInputBuffer(dequeue_input_result.index));
+ if (byte_buffer.IsNull() || byte_buffer.capacity() < size) {
+ SB_LOG(ERROR) << "Unable to write to MediaCodec input buffer.";
+ return false;
+ }
+ byte_buffer.CopyInto(data, size);
+ }
+
+ jint status;
+ if (event.type == Event::kWriteCodecConfig) {
+ status = media_codec_bridge_->QueueInputBuffer(dequeue_input_result.index,
+ kNoOffset, size, kNoPts,
+ BUFFER_FLAG_CODEC_CONFIG);
+ } else if (event.type == Event::kWriteInputBuffer) {
+ jlong pts_us = input_buffer->timestamp();
+ if (drm_system_ && input_buffer->drm_info()) {
+ status = media_codec_bridge_->QueueSecureInputBuffer(
+ dequeue_input_result.index, kNoOffset, *input_buffer->drm_info(),
+ pts_us);
+ } else {
+ status = media_codec_bridge_->QueueInputBuffer(
+ dequeue_input_result.index, kNoOffset, size, pts_us, kNoBufferFlags);
+ }
+ } else {
+ status = media_codec_bridge_->QueueInputBuffer(dequeue_input_result.index,
+ kNoOffset, size, kNoPts,
+ BUFFER_FLAG_END_OF_STREAM);
+ }
+
+ if (status != MEDIA_CODEC_OK) {
+ HandleError("queue(Secure)?InputBuffer", status);
+ // TODO: Stop the decoding loop on fatal error.
+ SB_DCHECK(!pending_queue_input_buffer_task_);
+ pending_queue_input_buffer_task_ = {dequeue_input_result, event};
+ return false;
+ }
+
+ return true;
+}
+
+bool MediaDecoder::DequeueAndProcessOutputBuffer() {
+ SB_CHECK(media_codec_bridge_);
+
+ DequeueOutputResult dequeue_output_result =
+ media_codec_bridge_->DequeueOutputBuffer(kDequeueTimeout);
+
+ // Note that if the |index| field of |DequeueOutputResult| is negative, then
+ // all fields other than |status| and |index| are invalid. This is
+ // especially important, as the Java side of |MediaCodecBridge| will reuse
+ // objects for returned results behind the scenes.
+ if (dequeue_output_result.index < 0) {
+ if (dequeue_output_result.status == MEDIA_CODEC_OUTPUT_FORMAT_CHANGED) {
+ host_->RefreshOutputFormat(media_codec_bridge_.get());
+ return true;
+ }
+
+ if (dequeue_output_result.status == MEDIA_CODEC_OUTPUT_BUFFERS_CHANGED) {
+ SB_DLOG(INFO) << "Output buffers changed, trying to dequeue again.";
+ return true;
+ }
+
+ HandleError("dequeueOutputBuffer", dequeue_output_result.status);
+ return false;
+ }
+
+ host_->ProcessOutputBuffer(media_codec_bridge_.get(), dequeue_output_result);
+ return true;
+}
+
+void MediaDecoder::HandleError(const char* action_name, jint status) {
+ SB_DCHECK(status != MEDIA_CODEC_OK);
+
+ bool retry = false;
+ if (status == MEDIA_CODEC_DEQUEUE_INPUT_AGAIN_LATER) {
+ // Don't bother logging a try again later status, it happens a lot.
+ return;
+ } else if (status == MEDIA_CODEC_DEQUEUE_OUTPUT_AGAIN_LATER) {
+ // Don't bother logging a try again later status, it will happen a lot.
+ return;
+ } else if (status == MEDIA_CODEC_NO_KEY) {
+ retry = true;
+ } else if (status == MEDIA_CODEC_INSUFFICIENT_OUTPUT_PROTECTION) {
+ drm_system_->OnInsufficientOutputProtection();
+ } else {
+ error_cb_(kSbPlayerErrorDecode,
+ FormatString("%s failed with status %d.", action_name, status));
+ }
+
+ if (retry) {
+ SB_LOG(INFO) << "|" << action_name << "| failed with status: "
+ << GetNameForMediaCodecStatus(status)
+ << ", will try again after a delay.";
+ } else {
+ SB_LOG(ERROR) << "|" << action_name << "| failed with status: "
+ << GetNameForMediaCodecStatus(status) << ".";
+ }
+}
+
+} // namespace shared
+} // namespace android
+} // namespace starboard
diff --git a/src/starboard/android/shared/media_decoder.h b/src/starboard/android/shared/media_decoder.h
new file mode 100644
index 0000000..b504ae6
--- /dev/null
+++ b/src/starboard/android/shared/media_decoder.h
@@ -0,0 +1,160 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_MEDIA_DECODER_H_
+#define STARBOARD_ANDROID_SHARED_MEDIA_DECODER_H_
+
+#include <jni.h>
+
+#include <deque>
+#include <functional>
+#include <queue>
+
+#include "starboard/android/shared/drm_system.h"
+#include "starboard/android/shared/media_codec_bridge.h"
+#include "starboard/atomic.h"
+#include "starboard/common/optional.h"
+#include "starboard/common/ref_counted.h"
+#include "starboard/media.h"
+#include "starboard/shared/internal_only.h"
+#include "starboard/shared/starboard/player/filter/common.h"
+#include "starboard/shared/starboard/player/input_buffer_internal.h"
+#include "starboard/shared/starboard/thread_checker.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+// TODO: Better encapsulation the MediaCodecBridge so the decoders no longer
+// need to talk directly to the MediaCodecBridge.
+class MediaDecoder {
+ public:
+ typedef ::starboard::shared::starboard::player::filter::ErrorCB ErrorCB;
+ typedef ::starboard::shared::starboard::player::InputBuffer InputBuffer;
+
+ // This class should be implemented by the users of MediaDecoder to receive
+ // various notifications. Note that all such functions are called on the
+ // decoder thread.
+ class Host {
+ public:
+ virtual void ProcessOutputBuffer(MediaCodecBridge* media_codec_bridge,
+ const DequeueOutputResult& output) = 0;
+ virtual void RefreshOutputFormat(MediaCodecBridge* media_codec_bridge) = 0;
+ // This function gets called frequently on the decoding thread to give the
+ // Host a chance to process when the MediaDecoder is decoding video.
+ // TODO: Revise the scheduling logic to give the host a chance to process in
+ // a more elegant way.
+ virtual bool Tick(MediaCodecBridge* media_codec_bridge) = 0;
+ // This function gets called before calling Flush() on the contained
+ // MediaCodecBridge so the host can have a chance to do necessary cleanups
+ // before the MediaCodecBridge is flushed.
+ virtual void OnFlushing() = 0;
+
+ protected:
+ ~Host() {}
+ };
+
+ MediaDecoder(Host* host,
+ SbMediaAudioCodec audio_codec,
+ const SbMediaAudioHeader& audio_header,
+ SbDrmSystem drm_system);
+ MediaDecoder(Host* host,
+ SbMediaVideoCodec video_codec,
+ int width,
+ int height,
+ jobject j_output_surface,
+ SbDrmSystem drm_system,
+ const SbMediaColorMetadata* color_metadata);
+ ~MediaDecoder();
+
+ void Initialize(const ErrorCB& error_cb);
+ void WriteInputBuffer(const scoped_refptr<InputBuffer>& input_buffer);
+ void WriteEndOfStream();
+
+ size_t GetNumberOfPendingTasks() const { return event_queue_.size(); }
+
+ bool is_valid() const { return media_codec_bridge_ != NULL; }
+
+ private:
+ struct Event {
+ enum Type {
+ kInvalid,
+ kWriteCodecConfig,
+ kWriteInputBuffer,
+ kWriteEndOfStream,
+ };
+
+ explicit Event(Type type = kInvalid) : type(type) {
+ SB_DCHECK(type != kWriteInputBuffer && type != kWriteCodecConfig);
+ }
+ Event(const int8_t* codec_config, int16_t codec_config_size)
+ : type(kWriteCodecConfig),
+ codec_config(codec_config),
+ codec_config_size(codec_config_size) {}
+ explicit Event(const scoped_refptr<InputBuffer>& input_buffer)
+ : type(kWriteInputBuffer), input_buffer(input_buffer) {}
+
+ Type type;
+ scoped_refptr<InputBuffer> input_buffer;
+ const int8_t* codec_config;
+ int16_t codec_config_size;
+ };
+
+ struct QueueInputBufferTask {
+ DequeueInputResult dequeue_input_result;
+ Event event;
+ };
+
+ static void* ThreadEntryPoint(void* context);
+ void DecoderThreadFunc();
+ void JoinOnDecoderThread();
+
+ void TeardownCodec();
+
+ bool ProcessOneInputBuffer(std::deque<Event>* pending_work);
+ // Attempt to dequeue a media codec output buffer. Returns whether the
+ // processing should continue. If a valid buffer is dequeued, it will call
+ // ProcessOutputBuffer() on host internally. It is the responsibility of
+ // ProcessOutputBuffer() to release the output buffer back to the system.
+ bool DequeueAndProcessOutputBuffer();
+
+ void HandleError(const char* action_name, jint status);
+
+ ::starboard::shared::starboard::ThreadChecker thread_checker_;
+
+ const SbMediaType media_type_;
+ Host* host_;
+ ErrorCB error_cb_;
+
+ // Working thread to avoid lengthy decoding work block the player thread.
+ SbThread decoder_thread_;
+ scoped_ptr<MediaCodecBridge> media_codec_bridge_;
+
+ bool stream_ended_;
+
+ DrmSystem* drm_system_;
+
+ // Events are processed in a queue, except for when handling events of type
+ // |kReset|, which are allowed to cut to the front.
+ EventQueue<Event> event_queue_;
+ atomic_bool destroying_;
+
+ optional<QueueInputBufferTask> pending_queue_input_buffer_task_;
+};
+
+} // namespace shared
+} // namespace android
+} // namespace starboard
+
+#endif // STARBOARD_ANDROID_SHARED_MEDIA_DECODER_H_
diff --git a/src/starboard/android/shared/media_get_audio_configuration.cc b/src/starboard/android/shared/media_get_audio_configuration.cc
new file mode 100644
index 0000000..ef59d08
--- /dev/null
+++ b/src/starboard/android/shared/media_get_audio_configuration.cc
@@ -0,0 +1,50 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/media.h"
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+
+using starboard::android::shared::JniEnvExt;
+using starboard::android::shared::ScopedLocalJavaRef;
+
+bool SbMediaGetAudioConfiguration(
+ int output_index,
+ SbMediaAudioConfiguration* out_configuration) {
+ if (output_index != 0 || out_configuration == NULL) {
+ return false;
+ }
+
+ out_configuration->index = 0;
+ out_configuration->connector = kSbMediaAudioConnectorHdmi;
+ out_configuration->latency = 0;
+ out_configuration->coding_type = kSbMediaAudioCodingTypePcm;
+
+ JniEnvExt* env = JniEnvExt::Get();
+ ScopedLocalJavaRef<jobject> j_audio_output_manager(
+ env->CallStarboardObjectMethodOrAbort(
+ "getAudioOutputManager", "()Ldev/cobalt/media/AudioOutputManager;"));
+ int channels = static_cast<int>(env->CallIntMethodOrAbort(
+ j_audio_output_manager.Get(), "getMaxChannels", "()I"));
+ if (channels < 2) {
+ SB_DLOG(WARNING)
+ << "The supported channels from output device is smaller than 2. "
+ "Fallback to 2 channels";
+ out_configuration->number_of_channels = 2;
+ } else {
+ out_configuration->number_of_channels = channels;
+ }
+ return true;
+}
diff --git a/src/starboard/android/shared/media_get_audio_output_count.cc b/src/starboard/android/shared/media_get_audio_output_count.cc
new file mode 100644
index 0000000..befefd3
--- /dev/null
+++ b/src/starboard/android/shared/media_get_audio_output_count.cc
@@ -0,0 +1,20 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/media.h"
+
+int SbMediaGetAudioOutputCount() {
+ // Only supports one HDMI output.
+ return 1;
+}
diff --git a/src/starboard/android/shared/media_get_initial_buffer_capacity.cc b/src/starboard/android/shared/media_get_initial_buffer_capacity.cc
new file mode 100644
index 0000000..1634373
--- /dev/null
+++ b/src/starboard/android/shared/media_get_initial_buffer_capacity.cc
@@ -0,0 +1,19 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/media.h"
+
+int SbMediaGetInitialBufferCapacity() {
+ return 80 * 1024 * 1024;
+}
diff --git a/src/starboard/android/shared/media_get_max_buffer_capacity.cc b/src/starboard/android/shared/media_get_max_buffer_capacity.cc
new file mode 100644
index 0000000..1d3899b
--- /dev/null
+++ b/src/starboard/android/shared/media_get_max_buffer_capacity.cc
@@ -0,0 +1,29 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/media.h"
+
+int SbMediaGetMaxBufferCapacity(SbMediaVideoCodec codec,
+ int resolution_width,
+ int resolution_height,
+ int bits_per_pixel) {
+ SB_UNREFERENCED_PARAMETER(codec);
+ SB_UNREFERENCED_PARAMETER(resolution_width);
+ SB_UNREFERENCED_PARAMETER(resolution_height);
+ SB_UNREFERENCED_PARAMETER(bits_per_pixel);
+ // TODO: refine this to a more reasonable value, taking into account
+ // resolution. On most platforms this is 36 * 1024 * 1024 for 1080p, and
+ // 65 * 1024 * 1024 for 4k.
+ return 500 * 1024 * 1024;
+}
diff --git a/src/starboard/android/shared/media_is_audio_supported.cc b/src/starboard/android/shared/media_is_audio_supported.cc
new file mode 100644
index 0000000..8994965
--- /dev/null
+++ b/src/starboard/android/shared/media_is_audio_supported.cc
@@ -0,0 +1,38 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/shared/starboard/media/media_support_internal.h"
+
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/android/shared/media_common.h"
+#include "starboard/configuration.h"
+#include "starboard/media.h"
+
+using starboard::android::shared::JniEnvExt;
+using starboard::android::shared::ScopedLocalJavaRef;
+using starboard::android::shared::SupportedAudioCodecToMimeType;
+
+SB_EXPORT bool SbMediaIsAudioSupported(SbMediaAudioCodec audio_codec,
+ int64_t bitrate) {
+ const char* mime = SupportedAudioCodecToMimeType(audio_codec);
+ if (!mime) {
+ return false;
+ }
+ JniEnvExt* env = JniEnvExt::Get();
+ ScopedLocalJavaRef<jstring> j_mime(env->NewStringStandardUTFOrAbort(mime));
+ return env->CallStaticBooleanMethodOrAbort(
+ "dev/cobalt/media/MediaCodecUtil", "hasAudioDecoderFor",
+ "(Ljava/lang/String;I)Z", j_mime.Get(),
+ static_cast<jint>(bitrate)) == JNI_TRUE;
+}
diff --git a/src/starboard/android/shared/media_is_output_protected.cc b/src/starboard/android/shared/media_is_output_protected.cc
new file mode 100644
index 0000000..141e75e
--- /dev/null
+++ b/src/starboard/android/shared/media_is_output_protected.cc
@@ -0,0 +1,26 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/media.h"
+
+#include "starboard/log.h"
+
+bool SbMediaIsOutputProtected() {
+ // Protected output is expected to be properly enforced by the system level
+ // DRM system, and also does not provide a convenient way to query its
+ // state. Thus, from a starboard application perspective, we always return
+ // |true|, and rely on playback of protected content failing at a later
+ // point if the output was not actually secure.
+ return true;
+}
diff --git a/src/starboard/android/shared/media_is_supported.cc b/src/starboard/android/shared/media_is_supported.cc
new file mode 100644
index 0000000..c7b150a
--- /dev/null
+++ b/src/starboard/android/shared/media_is_supported.cc
@@ -0,0 +1,31 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/media.h"
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/media_common.h"
+
+SB_EXPORT bool SbMediaIsSupported(SbMediaVideoCodec video_codec,
+ SbMediaAudioCodec audio_codec,
+ const char* key_system) {
+ using starboard::android::shared::IsWidevine;
+ using starboard::android::shared::JniEnvExt;
+ if (!IsWidevine(key_system)) {
+ return false;
+ }
+ return JniEnvExt::Get()->CallStaticBooleanMethodOrAbort(
+ "dev/cobalt/media/MediaDrmBridge",
+ "isWidevineCryptoSchemeSupported", "()Z") == JNI_TRUE;
+}
diff --git a/src/starboard/android/shared/media_is_video_supported.cc b/src/starboard/android/shared/media_is_video_supported.cc
new file mode 100644
index 0000000..ed29410
--- /dev/null
+++ b/src/starboard/android/shared/media_is_video_supported.cc
@@ -0,0 +1,93 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/shared/starboard/media/media_support_internal.h"
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/android/shared/media_common.h"
+#include "starboard/configuration.h"
+#include "starboard/media.h"
+
+using starboard::android::shared::JniEnvExt;
+using starboard::android::shared::ScopedLocalJavaRef;
+using starboard::android::shared::SupportedVideoCodecToMimeType;
+
+namespace {
+
+// https://developer.android.com/reference/android/view/Display.HdrCapabilities.html#HDR_TYPE_HDR10
+const jint HDR_TYPE_DOLBY_VISION = 1;
+const jint HDR_TYPE_HDR10 = 2;
+const jint HDR_TYPE_HLG = 3;
+
+bool IsTransferCharacteristicsSupported(SbMediaTransferId transfer_id) {
+ // Bt709 and unspecified are assumed to always be supported.
+ if (transfer_id == kSbMediaTransferIdBt709 ||
+ transfer_id == kSbMediaTransferIdUnspecified) {
+ return true;
+ }
+ // An HDR capable VP9 decoder is needed to handle HDR at all.
+ bool has_hdr_capable_vp9_decoder =
+ JniEnvExt::Get()->CallStaticBooleanMethodOrAbort(
+ "dev/cobalt/media/MediaCodecUtil", "hasHdrCapableVp9Decoder",
+ "()Z") == JNI_TRUE;
+ if (!has_hdr_capable_vp9_decoder) {
+ return false;
+ }
+
+ jint hdr_type;
+ if (transfer_id == kSbMediaTransferIdSmpteSt2084) {
+ hdr_type = HDR_TYPE_HDR10;
+ } else if (transfer_id == kSbMediaTransferIdAribStdB67) {
+ hdr_type = HDR_TYPE_HLG;
+ } else {
+ // No other transfer functions are supported, see
+ // https://source.android.com/devices/tech/display/hdr.
+ return false;
+ }
+
+ return JniEnvExt::Get()->CallStarboardBooleanMethodOrAbort(
+ "isHdrTypeSupported", "(I)Z", hdr_type) == JNI_TRUE;
+}
+
+} // namespace
+
+SB_EXPORT bool SbMediaIsVideoSupported(SbMediaVideoCodec video_codec,
+ int frame_width,
+ int frame_height,
+ int64_t bitrate,
+ int fps,
+ bool decode_to_texture_required,
+ SbMediaTransferId eotf) {
+ if (!IsTransferCharacteristicsSupported(eotf)) {
+ return false;
+ }
+ // While not necessarily true, for now we assume that all Android devices
+ // can play decode-to-texture video just as well as normal video.
+ SB_UNREFERENCED_PARAMETER(decode_to_texture_required);
+
+ const char* mime = SupportedVideoCodecToMimeType(video_codec);
+ if (!mime) {
+ return false;
+ }
+ JniEnvExt* env = JniEnvExt::Get();
+ ScopedLocalJavaRef<jstring> j_mime(env->NewStringStandardUTFOrAbort(mime));
+ bool must_support_hdr = (eotf != kSbMediaTransferIdBt709 &&
+ eotf != kSbMediaTransferIdUnspecified);
+ return env->CallStaticBooleanMethodOrAbort(
+ "dev/cobalt/media/MediaCodecUtil", "hasVideoDecoderFor",
+ "(Ljava/lang/String;ZIIIIZ)Z", j_mime.Get(), false, frame_width,
+ frame_height, static_cast<jint>(bitrate), fps,
+ must_support_hdr) == JNI_TRUE;
+}
diff --git a/src/starboard/android/shared/media_set_output_protection.cc b/src/starboard/android/shared/media_set_output_protection.cc
new file mode 100644
index 0000000..a3de2d9
--- /dev/null
+++ b/src/starboard/android/shared/media_set_output_protection.cc
@@ -0,0 +1,24 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/media.h"
+
+#include "starboard/log.h"
+
+bool SbMediaSetOutputProtection(bool enabled) {
+ // Output is always protected from a starboard application perspective, so
+ // return a redundant true. See documentation in |SbMediaIsOutputProtected|
+ // for further details.
+ return true;
+}
diff --git a/src/starboard/android/shared/microphone_impl.cc b/src/starboard/android/shared/microphone_impl.cc
new file mode 100644
index 0000000..ffff860
--- /dev/null
+++ b/src/starboard/android/shared/microphone_impl.cc
@@ -0,0 +1,540 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/shared/starboard/microphone/microphone_internal.h"
+
+#include <SLES/OpenSLES.h>
+#include <SLES/OpenSLES_Android.h>
+
+#include <algorithm>
+#include <queue>
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/common/scoped_ptr.h"
+#include "starboard/log.h"
+#include "starboard/memory.h"
+#include "starboard/shared/starboard/thread_checker.h"
+
+using starboard::android::shared::JniEnvExt;
+
+namespace starboard {
+namespace android {
+namespace shared {
+namespace {
+
+const int kSampleRateInHz = 16000;
+const int kSampleRateInMillihertz = kSampleRateInHz * 1000;
+const int kNumOfOpenSLESBuffers = 2;
+const int kSamplesPerBuffer = 128;
+const int kBufferSizeInBytes = kSamplesPerBuffer * sizeof(int16_t);
+
+bool CheckReturnValue(SLresult result) {
+ return result == SL_RESULT_SUCCESS;
+}
+} // namespace
+
+class SbMicrophoneImpl : public SbMicrophonePrivate {
+ public:
+ SbMicrophoneImpl();
+ ~SbMicrophoneImpl() SB_OVERRIDE;
+
+ bool Open() SB_OVERRIDE;
+ bool Close() SB_OVERRIDE;
+ int Read(void* out_audio_data, int audio_data_size) SB_OVERRIDE;
+
+ void SetPermission(bool is_granted);
+ static bool IsMicrophoneDisconnected();
+ static bool IsMicrophoneMute();
+
+ private:
+ enum State { kWaitPermission, kPermissionGranted, kOpened, kClosed };
+
+ static void SwapAndPublishBuffer(SLAndroidSimpleBufferQueueItf buffer_object,
+ void* context);
+ void SwapAndPublishBuffer();
+
+ bool CreateAudioRecorder();
+ void DeleteAudioRecorder();
+
+ void ClearBuffer();
+
+ bool RequestAudioPermission();
+ bool StartRecording();
+ bool StopRecording();
+
+ SLObjectItf engine_object_;
+ SLEngineItf engine_;
+ SLObjectItf recorder_object_;
+ SLRecordItf recorder_;
+ SLAndroidSimpleBufferQueueItf buffer_object_;
+ SLAndroidConfigurationItf config_object_;
+
+ // Keeps track of the microphone's current state.
+ State state_;
+ // Audio data that has been delivered to the buffer queue.
+ std::queue<int16_t*> delivered_queue_;
+ // Audio data that is ready to be read.
+ std::queue<int16_t*> ready_queue_;
+};
+
+SbMicrophoneImpl::SbMicrophoneImpl()
+ : engine_object_(nullptr),
+ engine_(nullptr),
+ recorder_object_(nullptr),
+ recorder_(nullptr),
+ buffer_object_(nullptr),
+ config_object_(nullptr),
+ state_(kClosed) {}
+
+SbMicrophoneImpl::~SbMicrophoneImpl() {
+ Close();
+}
+
+bool SbMicrophoneImpl::RequestAudioPermission() {
+ JniEnvExt* env = JniEnvExt::Get();
+ jobject j_audio_permission_requester =
+ static_cast<jobject>(env->CallStarboardObjectMethodOrAbort(
+ "getAudioPermissionRequester",
+ "()Ldev/cobalt/coat/AudioPermissionRequester;"));
+ jboolean j_permission = env->CallBooleanMethodOrAbort(
+ j_audio_permission_requester, "requestRecordAudioPermission", "(J)Z",
+ reinterpret_cast<intptr_t>(this));
+ return j_permission;
+}
+
+// static
+bool SbMicrophoneImpl::IsMicrophoneDisconnected() {
+ JniEnvExt* env = JniEnvExt::Get();
+ jboolean j_microphone =
+ env->CallStarboardBooleanMethodOrAbort("isMicrophoneDisconnected", "()Z");
+ return j_microphone;
+}
+
+// static
+bool SbMicrophoneImpl::IsMicrophoneMute() {
+ JniEnvExt* env = JniEnvExt::Get();
+ jboolean j_microphone =
+ env->CallStarboardBooleanMethodOrAbort("isMicrophoneMute", "()Z");
+ return j_microphone;
+}
+
+bool SbMicrophoneImpl::Open() {
+ if (state_ == kOpened) {
+ // The microphone has already been opened; clear the unread buffer. See
+ // starboard/microphone.h for more info.
+ ClearBuffer();
+ return true;
+ }
+
+ if (IsMicrophoneDisconnected()) {
+ SB_DLOG(WARNING) << "No microphone connected.";
+ return false;
+ } else if (!RequestAudioPermission()) {
+ state_ = kWaitPermission;
+ SB_DLOG(INFO) << "Waiting for audio permission.";
+ // The permission is not set; this causes the MicrophoneManager to call
+ // read() repeatedly and wait for the user's response.
+ return true;
+ } else if (!StartRecording()) {
+ SB_DLOG(WARNING) << "Error starting recording.";
+ return false;
+ }
+
+ // Successfully opened the microphone and started recording.
+ state_ = kOpened;
+ return true;
+}
+
+bool SbMicrophoneImpl::StartRecording() {
+ if (!CreateAudioRecorder()) {
+ SB_DLOG(WARNING) << "Create audio recorder failed.";
+ DeleteAudioRecorder();
+ return false;
+ }
+
+ // Enqueues kNumOfOpenSLESBuffers zero buffers to start.
+ // Adds buffers to the queue before changing state to ensure that recording
+ // starts as soon as the state is modified.
+ for (int i = 0; i < kNumOfOpenSLESBuffers; ++i) {
+ int16_t* buffer = new int16_t[kSamplesPerBuffer];
+ SbMemorySet(buffer, 0, kBufferSizeInBytes);
+ delivered_queue_.push(buffer);
+ SLresult result =
+ (*buffer_object_)->Enqueue(buffer_object_, buffer, kBufferSizeInBytes);
+ if (!CheckReturnValue(result)) {
+ SB_DLOG(WARNING) << "Error adding buffers to the queue.";
+ return false;
+ }
+ }
+
+ // Start the recording by setting the state to |SL_RECORDSTATE_RECORDING|.
+ // When the object is in the SL_RECORDSTATE_RECORDING state, adding buffers
+ // will implicitly start the recording process.
+ SLresult result =
+ (*recorder_)->SetRecordState(recorder_, SL_RECORDSTATE_RECORDING);
+ if (!CheckReturnValue(result)) {
+ return false;
+ }
+
+ return true;
+}
+
+bool SbMicrophoneImpl::Close() {
+ if (state_ == kClosed) {
+ // The microphone has already been closed.
+ return true;
+ }
+
+ if (state_ == kOpened && !StopRecording()) {
+ SB_DLOG(WARNING) << "Error closing the microphone.";
+ return false;
+ }
+
+ // Successfully closed the microphone and stopped recording.
+ state_ = kClosed;
+ return true;
+}
+
+bool SbMicrophoneImpl::StopRecording() {
+ // Stop recording by setting the record state to |SL_RECORDSTATE_STOPPED|.
+ SLresult result =
+ (*recorder_)->SetRecordState(recorder_, SL_RECORDSTATE_STOPPED);
+ if (!CheckReturnValue(result)) {
+ return false;
+ }
+
+ ClearBuffer();
+
+ DeleteAudioRecorder();
+
+ return true;
+}
+
+int SbMicrophoneImpl::Read(void* out_audio_data, int audio_data_size) {
+ if (state_ == kClosed || IsMicrophoneMute()) {
+ // No audio data is read from a stopped or muted microphone; return an
+ // error.
+ return -1;
+ }
+
+ if (!out_audio_data || audio_data_size == 0 || state_ == kWaitPermission) {
+ // No data to be read.
+ return 0;
+ }
+
+ if (state_ == kPermissionGranted) {
+ if (StartRecording()) {
+ state_ = kOpened;
+ } else {
+ // Could not start recording; return an error.
+ state_ = kClosed;
+ return -1;
+ }
+ }
+
+ int read_bytes = 0;
+ scoped_ptr<int16_t> buffer;
+ // Go through the ready queue, reading and sending audio data.
+ while (!ready_queue_.empty() &&
+ audio_data_size - read_bytes >= kBufferSizeInBytes) {
+ buffer.reset(ready_queue_.front());
+ SbMemoryCopy(static_cast<uint8_t*>(out_audio_data) + read_bytes,
+ buffer.get(), kBufferSizeInBytes);
+ ready_queue_.pop();
+ read_bytes += kBufferSizeInBytes;
+ }
+
+ buffer.reset();
+ return read_bytes;
+}
+
+void SbMicrophoneImpl::SetPermission(bool is_granted) {
+ state_ = is_granted ? kPermissionGranted : kClosed;
+}
+
+// static
+void SbMicrophoneImpl::SwapAndPublishBuffer(
+ SLAndroidSimpleBufferQueueItf buffer_object,
+ void* context) {
+ SbMicrophoneImpl* recorder = static_cast<SbMicrophoneImpl*>(context);
+ recorder->SwapAndPublishBuffer();
+}
+
+void SbMicrophoneImpl::SwapAndPublishBuffer() {
+ if (!delivered_queue_.empty()) {
+ // The front item in the delivered queue already has the buffered data, so
+ // move it from the delivered queue to the ready queue for future reads.
+ int16_t* buffer = delivered_queue_.front();
+ delivered_queue_.pop();
+ ready_queue_.push(buffer);
+ }
+
+ if (state_ == kOpened) {
+ int16_t* buffer = new int16_t[kSamplesPerBuffer];
+ SbMemorySet(buffer, 0, kBufferSizeInBytes);
+ delivered_queue_.push(buffer);
+ SLresult result =
+ (*buffer_object_)->Enqueue(buffer_object_, buffer, kBufferSizeInBytes);
+ CheckReturnValue(result);
+ }
+}
+
+bool SbMicrophoneImpl::CreateAudioRecorder() {
+ SLresult result;
+ // Initializes the SL engine object with specific options.
+ // OpenSL ES for Android is designed for multi-threaded applications and
+ // is thread-safe.
+ result = slCreateEngine(&engine_object_, 0, nullptr, 0, nullptr, nullptr);
+ if (!CheckReturnValue(result)) {
+ SB_DLOG(WARNING) << "Error creating the SL engine object.";
+ return false;
+ }
+
+ // Realize the SL engine object in synchronous mode.
+ result = (*engine_object_)
+ ->Realize(engine_object_, /* async = */ SL_BOOLEAN_FALSE);
+ if (!CheckReturnValue(result)) {
+ SB_DLOG(WARNING) << "Error realizing the SL engine object.";
+ return false;
+ }
+
+ // Get the SL engine interface.
+ result =
+ (*engine_object_)->GetInterface(engine_object_, SL_IID_ENGINE, &engine_);
+ if (!CheckReturnValue(result)) {
+ SB_DLOG(WARNING) << "Error getting the SL engine interface.";
+ return false;
+ }
+
+ // Audio source configuration; the audio source is an I/O device data locator.
+ SLDataLocator_IODevice input_dev_locator = {
+ SL_DATALOCATOR_IODEVICE, SL_IODEVICE_AUDIOINPUT,
+ SL_DEFAULTDEVICEID_AUDIOINPUT, nullptr};
+
+ SLDataSource audio_source = {&input_dev_locator, nullptr};
+ SLDataLocator_AndroidSimpleBufferQueue simple_buffer_queue = {
+ SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, kNumOfOpenSLESBuffers};
+
+ // Audio sink configuration; the audio sink is a simple buffer queue. PCM is
+ // the only data format allowed with buffer queues.
+ SLAndroidDataFormat_PCM_EX format = {
+ SL_ANDROID_DATAFORMAT_PCM_EX, 1 /* numChannels */,
+ kSampleRateInMillihertz, SL_PCMSAMPLEFORMAT_FIXED_16,
+ SL_PCMSAMPLEFORMAT_FIXED_16, SL_SPEAKER_FRONT_CENTER,
+ SL_BYTEORDER_LITTLEENDIAN, SL_ANDROID_PCM_REPRESENTATION_SIGNED_INT};
+ SLDataSink audio_sink = {&simple_buffer_queue, &format};
+
+ const int kCount = 2;
+ const SLInterfaceID kInterfaceId[kCount] = {SL_IID_ANDROIDSIMPLEBUFFERQUEUE,
+ SL_IID_ANDROIDCONFIGURATION};
+ const SLboolean kInterfaceRequired[kCount] = {SL_BOOLEAN_TRUE,
+ SL_BOOLEAN_TRUE};
+ // Create the audio recorder.
+ result = (*engine_)->CreateAudioRecorder(engine_, &recorder_object_,
+ &audio_source, &audio_sink, kCount,
+ kInterfaceId, kInterfaceRequired);
+ if (!CheckReturnValue(result)) {
+ SB_DLOG(WARNING) << "Error creating the audio recorder.";
+ return false;
+ }
+
+ // Configure the audio recorder (before it is realized); get the configuration
+ // interface.
+ result = (*recorder_object_)
+ ->GetInterface(recorder_object_, SL_IID_ANDROIDCONFIGURATION,
+ &config_object_);
+ if (!CheckReturnValue(result)) {
+ SB_DLOG(WARNING) << "Error getting the audio recorder interface.";
+ return false;
+ }
+
+ // Use the main microphone tuned for voice recognition.
+ const SLuint32 kPresetValue = SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION;
+ result =
+ (*config_object_)
+ ->SetConfiguration(config_object_, SL_ANDROID_KEY_RECORDING_PRESET,
+ &kPresetValue, sizeof(SLuint32));
+ if (!CheckReturnValue(result)) {
+ SB_DLOG(WARNING) << "Error configuring the audio recorder.";
+ return false;
+ }
+
+ // Realize the recorder in synchronous mode.
+ result = (*recorder_object_)
+ ->Realize(recorder_object_, /* async = */ SL_BOOLEAN_FALSE);
+ if (!CheckReturnValue(result)) {
+ SB_DLOG(WARNING) << "Error realizing the audio recorder. Double check that "
+ "the microphone is connected to the device.";
+ return false;
+ }
+
+ // Get the record interface (an implicit interface).
+ result = (*recorder_object_)
+ ->GetInterface(recorder_object_, SL_IID_RECORD, &recorder_);
+ if (!CheckReturnValue(result)) {
+ SB_DLOG(WARNING) << "Error getting the audio recorder interface.";
+ return false;
+ }
+
+ // Get the buffer queue interface which was explicitly requested.
+ result = (*recorder_object_)
+ ->GetInterface(recorder_object_, SL_IID_ANDROIDSIMPLEBUFFERQUEUE,
+ &buffer_object_);
+ if (!CheckReturnValue(result)) {
+ SB_DLOG(WARNING) << "Error getting the buffer queue interface.";
+ return false;
+ }
+
+ // Setup to receive buffer queue event callbacks for when a buffer in the
+ // queue is completed.
+ result =
+ (*buffer_object_)
+ ->RegisterCallback(buffer_object_,
+ &SbMicrophoneImpl::SwapAndPublishBuffer, this);
+ if (!CheckReturnValue(result)) {
+ SB_DLOG(WARNING) << "Error registering buffer queue callbacks.";
+ return false;
+ }
+
+ return true;
+}
+
+void SbMicrophoneImpl::DeleteAudioRecorder() {
+ if (recorder_object_) {
+ (*recorder_object_)->Destroy(recorder_object_);
+ }
+
+ config_object_ = nullptr;
+ buffer_object_ = nullptr;
+ recorder_ = nullptr;
+ recorder_object_ = nullptr;
+
+ // Destroy the engine object.
+ if (engine_object_) {
+ (*engine_object_)->Destroy(engine_object_);
+ }
+ engine_ = nullptr;
+ engine_object_ = nullptr;
+}
+
+void SbMicrophoneImpl::ClearBuffer() {
+ // Clear the buffer queue to get rid of old data.
+ if (buffer_object_) {
+ SLresult result = (*buffer_object_)->Clear(buffer_object_);
+ if (!CheckReturnValue(result)) {
+ SB_DLOG(WARNING) << "Error clearing the buffer.";
+ }
+ }
+
+ while (!delivered_queue_.empty()) {
+ delete[] delivered_queue_.front();
+ delivered_queue_.pop();
+ }
+
+ while (!ready_queue_.empty()) {
+ delete[] ready_queue_.front();
+ ready_queue_.pop();
+ }
+}
+
+} // namespace shared
+} // namespace android
+} // namespace starboard
+
+int SbMicrophonePrivate::GetAvailableMicrophones(
+ SbMicrophoneInfo* out_info_array,
+ int info_array_size) {
+ // Note that there is no way of checking for a connected microphone/device
+ // before API 23, so GetAvailableMicrophones() will assume a microphone is
+ // connected and always return 1 on APIs < 23.
+ if (starboard::android::shared::SbMicrophoneImpl::
+ IsMicrophoneDisconnected()) {
+ SB_DLOG(WARNING) << "No microphone connected.";
+ return 0;
+ }
+ if (starboard::android::shared::SbMicrophoneImpl::IsMicrophoneMute()) {
+ SB_DLOG(WARNING) << "Microphone is muted.";
+ return 0;
+ }
+
+ if (out_info_array && info_array_size > 0) {
+ // Only support one microphone.
+ out_info_array[0].id = reinterpret_cast<SbMicrophoneId>(1);
+ out_info_array[0].type = kSbMicrophoneUnknown;
+ out_info_array[0].max_sample_rate_hz =
+ starboard::android::shared::kSampleRateInHz;
+ out_info_array[0].min_read_size =
+ starboard::android::shared::kSamplesPerBuffer;
+ }
+
+ return 1;
+}
+
+bool SbMicrophonePrivate::IsMicrophoneSampleRateSupported(
+ SbMicrophoneId id,
+ int sample_rate_in_hz) {
+ if (!SbMicrophoneIdIsValid(id)) {
+ return false;
+ }
+
+ return sample_rate_in_hz == starboard::android::shared::kSampleRateInHz;
+}
+
+namespace {
+const int kUnusedBufferSize = 32 * 1024;
+// Only a single microphone is supported.
+SbMicrophone s_microphone = kSbMicrophoneInvalid;
+
+} // namespace
+
+SbMicrophone SbMicrophonePrivate::CreateMicrophone(SbMicrophoneId id,
+ int sample_rate_in_hz,
+ int buffer_size_bytes) {
+ if (!SbMicrophoneIdIsValid(id) ||
+ !IsMicrophoneSampleRateSupported(id, sample_rate_in_hz) ||
+ buffer_size_bytes > kUnusedBufferSize || buffer_size_bytes <= 0) {
+ return kSbMicrophoneInvalid;
+ }
+
+ if (s_microphone != kSbMicrophoneInvalid) {
+ return kSbMicrophoneInvalid;
+ }
+
+ s_microphone = new starboard::android::shared::SbMicrophoneImpl();
+ return s_microphone;
+}
+
+void SbMicrophonePrivate::DestroyMicrophone(SbMicrophone microphone) {
+ if (!SbMicrophoneIsValid(microphone)) {
+ return;
+ }
+
+ SB_DCHECK(s_microphone == microphone);
+ s_microphone->Close();
+
+ delete s_microphone;
+ s_microphone = kSbMicrophoneInvalid;
+}
+
+extern "C" SB_EXPORT_PLATFORM void
+Java_dev_cobalt_coat_AudioPermissionRequester_nativeHandlePermission(
+ JNIEnv* env,
+ jobject unused_this,
+ jlong nativeSbMicrophoneImpl,
+ jboolean is_granted) {
+ starboard::android::shared::SbMicrophoneImpl* native =
+ reinterpret_cast<starboard::android::shared::SbMicrophoneImpl*>(
+ nativeSbMicrophoneImpl);
+ native->SetPermission(is_granted);
+}
diff --git a/src/starboard/android/shared/platform_deploy.gypi b/src/starboard/android/shared/platform_deploy.gypi
new file mode 100644
index 0000000..3d6e60f
--- /dev/null
+++ b/src/starboard/android/shared/platform_deploy.gypi
@@ -0,0 +1,68 @@
+# Copyright 2017 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+{
+ 'variables': {
+ 'GRADLE_BUILD_TYPE': '<(cobalt_config)',
+ 'GRADLE_FILES_DIR': '<(PRODUCT_DIR)/gradle/<(executable_name)',
+ 'executable_file': '<(PRODUCT_DIR)/lib/lib<(executable_name).so',
+ 'apk': '<(PRODUCT_DIR)/<(executable_name).apk',
+ },
+ 'conditions': [
+ [ 'cobalt_config == "gold"', {
+ 'variables': {
+ # Android Gradle wants a "release" build type, so use that for "gold".
+ 'GRADLE_BUILD_TYPE': 'release'
+ }
+ }]
+ ],
+ 'dependencies': [ '<(DEPTH)/starboard/android/apk/apk.gyp:apk_sources' ],
+ 'includes': [ '<(DEPTH)/starboard/build/collect_deploy_content.gypi' ],
+ 'actions': [
+ {
+ 'action_name': 'build_apk',
+ 'inputs': [
+ '<(executable_file)',
+ '<(PRODUCT_DIR)/gradle/apk_sources.stamp',
+ '<(content_deploy_stamp_file)',
+ ],
+ 'outputs': [ '<(apk)' ],
+ 'action': [
+ '<(DEPTH)/starboard/android/apk/cobalt-gradle.sh',
+ '--sdk', '<(ANDROID_HOME)',
+ '--ndk', '<(NDK_HOME)',
+ '--cache', '<(GRADLE_FILES_DIR)/cache',
+ '--project-dir', '<(DEPTH)/starboard/android/apk/',
+ '-P', 'cobaltBuildAbi=<(ANDROID_ABI)',
+ '-P', 'cobaltDeployApk=<(apk)',
+ '-P', 'cobaltContentDir=<(content_deploy_dir)',
+ '-P', 'cobaltGradleDir=<(GRADLE_FILES_DIR)',
+ '-P', 'cobaltProductDir=<(PRODUCT_DIR)',
+ '-P', 'cobaltTarget=<(executable_name)',
+ 'assembleCobalt_<(GRADLE_BUILD_TYPE)',
+ ],
+ 'message': 'Build APK: <(apk)',
+ },
+ {
+ # Clean the gradle directory to conserve space after we have the APK
+ 'action_name': 'delete_gradle_dir',
+ 'inputs': [ '<(apk)' ],
+ 'outputs': [ '<(GRADLE_FILES_DIR).deleted.stamp' ],
+ 'action': [
+ 'sh', '-c',
+ 'rm -rf <(GRADLE_FILES_DIR) && touch <(GRADLE_FILES_DIR).deleted.stamp',
+ ],
+ 'message': 'Cleanup Gradle: <(executable_name)',
+ },
+ ],
+}
diff --git a/src/starboard/android/shared/player_components_impl.cc b/src/starboard/android/shared/player_components_impl.cc
new file mode 100644
index 0000000..0d14113
--- /dev/null
+++ b/src/starboard/android/shared/player_components_impl.cc
@@ -0,0 +1,109 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/shared/starboard/player/filter/player_components.h"
+
+#include "starboard/android/shared/audio_decoder.h"
+#include "starboard/android/shared/video_decoder.h"
+#include "starboard/android/shared/video_render_algorithm.h"
+#include "starboard/common/ref_counted.h"
+#include "starboard/common/scoped_ptr.h"
+#include "starboard/media.h"
+#include "starboard/shared/starboard/player/filter/audio_decoder_internal.h"
+#include "starboard/shared/starboard/player/filter/audio_renderer_sink.h"
+#include "starboard/shared/starboard/player/filter/audio_renderer_sink_impl.h"
+#include "starboard/shared/starboard/player/filter/video_decoder_internal.h"
+#include "starboard/shared/starboard/player/filter/video_render_algorithm.h"
+#include "starboard/shared/starboard/player/filter/video_render_algorithm_impl.h"
+#include "starboard/shared/starboard/player/filter/video_renderer_sink.h"
+
+namespace starboard {
+namespace shared {
+namespace starboard {
+namespace player {
+namespace filter {
+
+namespace {
+
+class PlayerComponentsImpl : public PlayerComponents {
+ void CreateAudioComponents(
+ const AudioParameters& audio_parameters,
+ scoped_ptr<AudioDecoder>* audio_decoder,
+ scoped_ptr<AudioRendererSink>* audio_renderer_sink) override {
+ using AudioDecoderImpl = ::starboard::android::shared::AudioDecoder;
+
+ SB_DCHECK(audio_decoder);
+ SB_DCHECK(audio_renderer_sink);
+
+ scoped_ptr<AudioDecoderImpl> audio_decoder_impl(new AudioDecoderImpl(
+ audio_parameters.audio_codec, audio_parameters.audio_header,
+ audio_parameters.drm_system));
+ if (audio_decoder_impl->is_valid()) {
+ audio_decoder->reset(audio_decoder_impl.release());
+ } else {
+ audio_decoder->reset();
+ }
+ audio_renderer_sink->reset(new AudioRendererSinkImpl);
+ }
+
+ void CreateVideoComponents(
+ const VideoParameters& video_parameters,
+ scoped_ptr<VideoDecoder>* video_decoder,
+ scoped_ptr<VideoRenderAlgorithm>* video_render_algorithm,
+ scoped_refptr<VideoRendererSink>* video_renderer_sink) override {
+ using VideoDecoderImpl = ::starboard::android::shared::VideoDecoder;
+ using VideoRenderAlgorithmImpl =
+ ::starboard::android::shared::VideoRenderAlgorithm;
+
+ SB_DCHECK(video_decoder);
+ SB_DCHECK(video_render_algorithm);
+ SB_DCHECK(video_renderer_sink);
+
+ scoped_ptr<VideoDecoderImpl> video_decoder_impl(new VideoDecoderImpl(
+ video_parameters.video_codec, video_parameters.drm_system,
+ video_parameters.output_mode,
+ video_parameters.decode_target_graphics_context_provider));
+ if (video_decoder_impl->is_valid()) {
+ *video_renderer_sink = video_decoder_impl->GetSink();
+ video_decoder->reset(video_decoder_impl.release());
+ } else {
+ video_decoder->reset();
+ *video_renderer_sink = NULL;
+ }
+
+ video_render_algorithm->reset(new VideoRenderAlgorithmImpl);
+ }
+
+ void GetAudioRendererParams(int* max_cached_frames,
+ int* max_frames_per_append) const override {
+ SB_DCHECK(max_cached_frames);
+ SB_DCHECK(max_frames_per_append);
+
+ *max_cached_frames = 256 * 1024;
+ *max_frames_per_append = 16384;
+ }
+};
+
+} // namespace
+
+// static
+scoped_ptr<PlayerComponents> PlayerComponents::Create() {
+ return make_scoped_ptr<PlayerComponents>(new PlayerComponentsImpl);
+}
+
+} // namespace filter
+} // namespace player
+} // namespace starboard
+} // namespace shared
+} // namespace starboard
diff --git a/src/starboard/android/shared/player_create.cc b/src/starboard/android/shared/player_create.cc
new file mode 100644
index 0000000..7db4b61
--- /dev/null
+++ b/src/starboard/android/shared/player_create.cc
@@ -0,0 +1,116 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/player.h"
+
+#include "starboard/android/shared/cobalt/android_media_session_client.h"
+#include "starboard/configuration.h"
+#include "starboard/decode_target.h"
+#include "starboard/log.h"
+#include "starboard/shared/starboard/player/filter/filter_based_player_worker_handler.h"
+#include "starboard/shared/starboard/player/player_internal.h"
+#include "starboard/shared/starboard/player/player_worker.h"
+
+using starboard::shared::starboard::player::filter::
+ FilterBasedPlayerWorkerHandler;
+using starboard::shared::starboard::player::PlayerWorker;
+using starboard::android::shared::cobalt::kPlaying;
+using starboard::android::shared::cobalt::
+ UpdateActiveSessionPlatformPlaybackState;
+using starboard::android::shared::cobalt::UpdateActiveSessionPlatformPlayer;
+
+SbPlayer SbPlayerCreate(SbWindow window,
+ SbMediaVideoCodec video_codec,
+ SbMediaAudioCodec audio_codec,
+#if SB_API_VERSION < 10
+ SbMediaTime duration_pts,
+#endif // SB_API_VERSION < 10
+ SbDrmSystem drm_system,
+ const SbMediaAudioHeader* audio_header,
+ SbPlayerDeallocateSampleFunc sample_deallocate_func,
+ SbPlayerDecoderStatusFunc decoder_status_func,
+ SbPlayerStatusFunc player_status_func,
+ SbPlayerErrorFunc player_error_func,
+ void* context,
+ SbPlayerOutputMode output_mode,
+ SbDecodeTargetGraphicsContextProvider* provider) {
+ SB_UNREFERENCED_PARAMETER(window);
+ SB_UNREFERENCED_PARAMETER(provider);
+#if SB_API_VERSION < 10
+ SB_UNREFERENCED_PARAMETER(duration_pts);
+#endif // SB_API_VERSION < 10
+
+ if (!sample_deallocate_func || !decoder_status_func || !player_status_func
+#if SB_HAS(PLAYER_ERROR_MESSAGE)
+ || !player_error_func
+#endif // SB_HAS(PLAYER_ERROR_MESSAGE)
+ ) {
+ return kSbPlayerInvalid;
+ }
+
+ if (audio_codec != kSbMediaAudioCodecNone &&
+ audio_codec != kSbMediaAudioCodecAac) {
+ SB_LOG(ERROR) << "Unsupported audio codec " << audio_codec;
+ return kSbPlayerInvalid;
+ }
+
+ if (audio_codec != kSbMediaAudioCodecNone && !audio_header) {
+ SB_LOG(ERROR) << "SbPlayerCreate() requires a non-NULL SbMediaAudioHeader "
+ << "when |audio_codec| is not kSbMediaAudioCodecNone";
+ return kSbPlayerInvalid;
+ }
+
+ if (video_codec != kSbMediaVideoCodecNone &&
+ video_codec != kSbMediaVideoCodecH264 &&
+ video_codec != kSbMediaVideoCodecVp9) {
+ SB_LOG(ERROR) << "Unsupported video codec " << video_codec;
+ return kSbPlayerInvalid;
+ }
+
+ if (audio_codec == kSbMediaAudioCodecNone &&
+ video_codec == kSbMediaVideoCodecNone) {
+ SB_LOG(ERROR) << "SbPlayerCreate() requires at least one audio track or"
+ << " one video track.";
+ return kSbPlayerInvalid;
+ }
+
+ if (!SbPlayerOutputModeSupported(output_mode, video_codec, drm_system)) {
+ SB_LOG(ERROR) << "Unsupported player output mode " << output_mode;
+ return kSbPlayerInvalid;
+ }
+
+ // TODO: increase this once we support multiple video windows.
+ const int kMaxNumberOfPlayers = 1;
+ if (SbPlayerPrivate::number_of_players() >= kMaxNumberOfPlayers) {
+ return kSbPlayerInvalid;
+ }
+
+ UpdateActiveSessionPlatformPlaybackState(kPlaying);
+
+ starboard::scoped_ptr<PlayerWorker::Handler> handler(
+ new FilterBasedPlayerWorkerHandler(video_codec, audio_codec, drm_system,
+ audio_header, output_mode, provider));
+ SbPlayer player = SbPlayerPrivate::CreateInstance(
+ audio_codec, video_codec, sample_deallocate_func, decoder_status_func,
+ player_status_func, player_error_func, context, handler.Pass());
+
+ // TODO: accomplish this through more direct means.
+ // Set the bounds to initialize the VideoSurfaceView. The initial values don't
+ // matter.
+ SbPlayerSetBounds(player, 0, 0, 0, 0, 0);
+
+ UpdateActiveSessionPlatformPlayer(player);
+
+ return player;
+}
diff --git a/src/starboard/android/shared/player_destroy.cc b/src/starboard/android/shared/player_destroy.cc
new file mode 100644
index 0000000..b1b5a96
--- /dev/null
+++ b/src/starboard/android/shared/player_destroy.cc
@@ -0,0 +1,32 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/player.h"
+
+#include "starboard/android/shared/cobalt/android_media_session_client.h"
+#include "starboard/shared/starboard/player/player_internal.h"
+
+using starboard::android::shared::cobalt::kNone;
+using starboard::android::shared::cobalt::
+ UpdateActiveSessionPlatformPlaybackState;
+using starboard::android::shared::cobalt::UpdateActiveSessionPlatformPlayer;
+
+void SbPlayerDestroy(SbPlayer player) {
+ if (!SbPlayerIsValid(player)) {
+ return;
+ }
+ UpdateActiveSessionPlatformPlaybackState(kNone);
+ UpdateActiveSessionPlatformPlayer(kSbPlayerInvalid);
+ delete player;
+}
diff --git a/src/starboard/android/shared/player_set_bounds.cc b/src/starboard/android/shared/player_set_bounds.cc
new file mode 100644
index 0000000..d783a66
--- /dev/null
+++ b/src/starboard/android/shared/player_set_bounds.cc
@@ -0,0 +1,34 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/player.h"
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/log.h"
+#include "starboard/shared/starboard/player/player_internal.h"
+
+void SbPlayerSetBounds(SbPlayer player,
+ int z_index,
+ int x,
+ int y,
+ int width,
+ int height) {
+ if (!SbPlayerIsValid(player)) {
+ SB_DLOG(WARNING) << "player is invalid.";
+ return;
+ }
+ starboard::android::shared::JniEnvExt::Get()->CallStarboardVoidMethodOrAbort(
+ "setVideoSurfaceBounds", "(IIII)V", x, y, width, height);
+ player->SetBounds(z_index, x, y, width, height);
+}
diff --git a/src/starboard/android/shared/player_set_playback_rate.cc b/src/starboard/android/shared/player_set_playback_rate.cc
new file mode 100644
index 0000000..413c093
--- /dev/null
+++ b/src/starboard/android/shared/player_set_playback_rate.cc
@@ -0,0 +1,41 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/player.h"
+
+#include "starboard/android/shared/cobalt/android_media_session_client.h"
+#include "starboard/log.h"
+#include "starboard/shared/starboard/player/player_internal.h"
+
+using starboard::android::shared::cobalt::kPaused;
+using starboard::android::shared::cobalt::kPlaying;
+using starboard::android::shared::cobalt::
+ UpdateActiveSessionPlatformPlaybackState;
+
+bool SbPlayerSetPlaybackRate(SbPlayer player, double playback_rate) {
+ if (!SbPlayerIsValid(player)) {
+ SB_DLOG(WARNING) << "player is invalid.";
+ return false;
+ }
+ if (playback_rate < 0.0) {
+ SB_DLOG(WARNING) << "playback_rate cannot be negative but it is set to "
+ << playback_rate << '.';
+ return false;
+ }
+ bool paused = (playback_rate == 0.0);
+ UpdateActiveSessionPlatformPlaybackState(paused ? kPaused : kPlaying);
+
+ player->SetPlaybackRate(playback_rate);
+ return true;
+}
diff --git a/src/starboard/android/shared/sanitizer_options.cc b/src/starboard/android/shared/sanitizer_options.cc
new file mode 100644
index 0000000..aafbc54
--- /dev/null
+++ b/src/starboard/android/shared/sanitizer_options.cc
@@ -0,0 +1,42 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Removes gallium leak warnings from x11 GL code, but defines it as a weak
+// symbol, so other code can override it if they want to.
+
+#if defined(ADDRESS_SANITIZER)
+
+// Functions returning default options are declared weak in the tools' runtime
+// libraries. To make the linker pick the strong replacements for those
+// functions from this module, we explicitly force its inclusion by passing
+// -Wl,-u_sanitizer_options_link_helper
+extern "C" void _sanitizer_options_link_helper() { }
+
+#define SANITIZER_HOOK_ATTRIBUTE \
+ extern "C" \
+ __attribute__((no_sanitize_address)) \
+ __attribute__((no_sanitize_memory)) \
+ __attribute__((no_sanitize_thread)) \
+ __attribute__((visibility("default"))) \
+ __attribute__((weak)) \
+ __attribute__((used))
+
+// Newline separated list of issues to suppress, see
+// http://clang.llvm.org/docs/AddressSanitizer.html#issue-suppression
+// http://llvm.org/svn/llvm-project/compiler-rt/trunk/lib/sanitizer_common/sanitizer_suppressions.cc
+SANITIZER_HOOK_ATTRIBUTE const char* __lsan_default_suppressions() {
+ return "leak:egl_gallium.so\n";
+}
+
+#endif // defined(ADDRESS_SANITIZER)
diff --git a/src/starboard/android/shared/sdk_utils.py b/src/starboard/android/shared/sdk_utils.py
new file mode 100644
index 0000000..025e8df
--- /dev/null
+++ b/src/starboard/android/shared/sdk_utils.py
@@ -0,0 +1,348 @@
+# Copyright 2016 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Utilities to use the toolchain from the Android NDK."""
+
+import ConfigParser
+import fcntl
+import hashlib
+import logging
+import os
+import re
+import shutil
+import StringIO
+import subprocess
+import sys
+import time
+import urllib
+import zipfile
+
+from starboard.tools import build
+
+# The API level of NDK standalone toolchain to install. This should be the
+# minimum API level on which the app is expected to run. If some feature from a
+# newer NDK level is needed, this may be increased with caution.
+# https://developer.android.com/ndk/guides/stable_apis.html
+#
+# Using 24 will lead to missing symbols on API 23 devices.
+# https://github.com/android-ndk/ndk/issues/126
+_ANDROID_NDK_API_LEVEL = '21'
+
+# Packages to install in the Android SDK.
+# Get available packages from "sdkmanager --list --verbose"
+_ANDROID_SDK_PACKAGES = [
+ 'build-tools;28.0.3',
+ 'cmake;3.6.4111459',
+ 'emulator',
+ 'extras;android;m2repository',
+ 'extras;google;m2repository',
+ 'lldb;3.1',
+ 'ndk-bundle',
+ 'patcher;v4',
+ 'platforms;android-28',
+ 'platform-tools',
+ 'tools',
+]
+
+# Seconds to sleep before writing "y" for android sdk update license prompt.
+_SDK_LICENSE_PROMPT_SLEEP_SECONDS = 5
+
+# Location from which to download the SDK command-line tools
+# see https://developer.android.com/studio/index.html#command-tools
+_SDK_URL = 'https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip'
+
+_STARBOARD_TOOLCHAINS_DIR = build.GetToolchainsDir()
+
+# The path to the Android SDK, if placed inside of starboard-toolchains.
+_STARBOARD_TOOLCHAINS_SDK_DIR = os.path.join(_STARBOARD_TOOLCHAINS_DIR,
+ 'AndroidSdk')
+
+_ANDROID_HOME = os.environ.get('ANDROID_HOME')
+if _ANDROID_HOME:
+ _SDK_PATH = _ANDROID_HOME
+else:
+ _SDK_PATH = _STARBOARD_TOOLCHAINS_SDK_DIR
+
+_ANDROID_NDK_HOME = os.environ.get('ANDROID_NDK_HOME')
+if _ANDROID_NDK_HOME:
+ _NDK_PATH = _ANDROID_NDK_HOME
+else:
+ _NDK_PATH = os.path.join(_SDK_PATH, 'ndk-bundle')
+
+_SDKMANAGER_TOOL = os.path.join(_SDK_PATH, 'tools', 'bin', 'sdkmanager')
+
+# Maps the Android ABI to the architecture name of the toolchain.
+_TOOLS_ABI_ARCH_MAP = {
+ 'x86': 'x86',
+ 'armeabi': 'arm',
+ 'armeabi-v7a': 'arm',
+ 'arm64-v8a': 'arm64',
+}
+
+_SCRIPT_HASH_PROPERTY = 'SdkUtils.Hash'
+
+with open(__file__, 'rb') as script:
+ _SCRIPT_HASH = hashlib.md5(script.read()).hexdigest()
+
+
+def GetToolsPath(abi):
+ """Returns the path where the NDK standalone toolchain should be."""
+ tools_arch = _TOOLS_ABI_ARCH_MAP[abi]
+ tools_dir = 'android_toolchain_api{}_{}'.format(_ANDROID_NDK_API_LEVEL,
+ tools_arch)
+ return os.path.realpath(os.path.join(_STARBOARD_TOOLCHAINS_DIR, tools_dir))
+
+
+def _CheckStamp(dir_path):
+ """Checks that the specified directory is up-to-date with the NDK."""
+ stamp_path = os.path.join(dir_path, 'ndk.stamp')
+ return (os.path.exists(stamp_path) and
+ _ReadNdkRevision(stamp_path) == _GetInstalledNdkRevision() and
+ _ReadProperty(stamp_path, _SCRIPT_HASH_PROPERTY) == _SCRIPT_HASH)
+
+
+def _UpdateStamp(dir_path):
+ """Updates the stamp file in the specified directory to the NDK revision."""
+ path = GetNdkPath()
+ properties_path = os.path.join(path, 'source.properties')
+ stamp_path = os.path.join(dir_path, 'ndk.stamp')
+ shutil.copyfile(properties_path, stamp_path)
+ with open(stamp_path, 'a') as stamp:
+ stamp.write('{} = {}\n'.format(_SCRIPT_HASH_PROPERTY, _SCRIPT_HASH))
+
+
+def GetNdkPath():
+ return _NDK_PATH
+
+
+def GetSdkPath():
+ return _SDK_PATH
+
+
+def _ReadNdkRevision(properties_path):
+ return _ReadProperty(properties_path, 'pkg.revision')
+
+
+def _ReadProperty(properties_path, property_key):
+ with open(properties_path, 'r') as f:
+ ini_str = '[properties]\n' + f.read()
+ config = ConfigParser.RawConfigParser()
+ config.readfp(StringIO.StringIO(ini_str))
+ try:
+ return config.get('properties', property_key)
+ except ConfigParser.NoOptionError:
+ return None
+
+
+def _GetInstalledNdkRevision():
+ """Returns the installed NDK's revision."""
+ path = GetNdkPath()
+ properties_path = os.path.join(path, 'source.properties')
+ try:
+ return _ReadNdkRevision(properties_path)
+ except IOError:
+ logging.error("Error: Can't read NDK properties in %s", properties_path)
+ sys.exit(1)
+
+
+def InstallSdkIfNeeded(abi):
+ """Installs appropriate SDK/NDK and NDK standalone tools if needed."""
+ _MaybeDownloadAndInstallSdkAndNdk()
+ _MaybeMakeToolchain(abi)
+
+
+def _DownloadAndUnzipFile(url, destination_path):
+ dl_file, dummy_headers = urllib.urlretrieve(url)
+ _UnzipFile(dl_file, destination_path)
+
+
+def _UnzipFile(zip_path, dest_path):
+ """Extract all files and restore permissions from a zip file."""
+ zip_file = zipfile.ZipFile(zip_path)
+ for info in zip_file.infolist():
+ zip_file.extract(info.filename, dest_path)
+ os.chmod(os.path.join(dest_path, info.filename), info.external_attr >> 16L)
+
+
+def _MaybeDownloadAndInstallSdkAndNdk():
+ """Download the SDK and NDK if not already available."""
+ # Hold an exclusive advisory lock on the _STARBOARD_TOOLCHAINS_DIR, to
+ # prevent issues with modification for multiple variants.
+ try:
+ toolchains_dir_fd = os.open(_STARBOARD_TOOLCHAINS_DIR, os.O_RDONLY)
+ fcntl.flock(toolchains_dir_fd, fcntl.LOCK_EX)
+
+ if _ANDROID_HOME:
+ if not os.access(_SDKMANAGER_TOOL, os.X_OK):
+ logging.error('Error: ANDROID_HOME is set but SDK is not present!')
+ sys.exit(1)
+ logging.warning('Warning: Using Android SDK in ANDROID_HOME,'
+ ' which is not automatically updated.\n'
+ ' The following package versions are installed:')
+ installed_packages = _GetInstalledSdkPackages()
+ for package in _ANDROID_SDK_PACKAGES:
+ version = installed_packages.get(package, '< MISSING! >')
+ msg = ' {:30} : {}'.format(package, version)
+ logging.warning(msg)
+ else:
+ logging.warning('Checking Android SDK.')
+ _DownloadInstallOrUpdateSdk()
+
+ if _ANDROID_NDK_HOME:
+ logging.warning('Warning: Using Android NDK in ANDROID_NDK_HOME,'
+ ' which is not automatically updated')
+ ndk_revision = _GetInstalledNdkRevision()
+ logging.warning('Using Android NDK version %s', ndk_revision)
+
+ if _ANDROID_HOME or _ANDROID_NDK_HOME:
+ reply = raw_input(
+ 'Do you want to continue using your custom Android tools? [yN]')
+ if reply.upper() != 'Y':
+ sys.exit(1)
+
+ finally:
+ fcntl.flock(toolchains_dir_fd, fcntl.LOCK_UN)
+ os.close(toolchains_dir_fd)
+
+
+def _GetInstalledSdkPackages():
+ """Returns a map of installed package name to package version."""
+ # We detect and parse either new-style or old-style output from sdkmanager:
+ #
+ # [new-style]
+ # Installed packages:
+ # --------------------------------------
+ # build-tools;25.0.2
+ # Description: Android SDK Build-Tools 25.0.2
+ # Version: 25.0.2
+ # Installed Location: <abs path>
+ # ...
+ #
+ # [old-style]
+ # Installed packages:
+ # Path | Version | Description | Location
+ # ------- | ------- | ------- | -------
+ # build-tools;25.0.2 | 25.0.2 | Android SDK Build-Tools 25.0.2 | <rel path>
+ # ...
+ section_re = re.compile(r'^[A-Z][^:]*:$')
+ version_re = re.compile(r'^\s+Version:\s+(\S+)')
+
+ p = subprocess.Popen(
+ [_SDKMANAGER_TOOL, '--list', '--verbose'], stdout=subprocess.PIPE)
+
+ installed_package_versions = {}
+ new_style = False
+ old_style = False
+ for line in iter(p.stdout.readline, ''):
+
+ if section_re.match(line):
+ if new_style or old_style:
+ # We left the new/old style installed packages section
+ break
+ if line.startswith('Installed'):
+ # Read the header of this section to determine new/old style
+ line = p.stdout.readline().strip()
+ new_style = line.startswith('----')
+ old_style = line.startswith('Path')
+ if old_style:
+ line = p.stdout.readline().strip()
+ if not line.startswith('----'):
+ logging.error('Error: Unexpected SDK package listing format')
+ continue
+
+ # We're in the installed packages section, and it's new-style
+ if new_style:
+ if not line.startswith(' '):
+ pkg_name = line.strip()
+ else:
+ m = version_re.match(line)
+ if m:
+ installed_package_versions[pkg_name] = m.group(1)
+
+ # We're in the installed packages section, and it's old-style
+ elif old_style:
+ fields = [f.strip() for f in line.split('|', 2)]
+ if len(fields) >= 2:
+ installed_package_versions[fields[0]] = fields[1]
+
+ # Discard the rest of the output
+ p.communicate()
+ return installed_package_versions
+
+
+def _IsOnBuildbot():
+ return 'BUILDBOT_BUILDERNAME' in os.environ
+
+
+def _DownloadInstallOrUpdateSdk():
+ """Downloads (if necessary) and installs/updates the Android SDK."""
+
+ # If we can't access the "sdkmanager" tool, we need to download the SDK
+ if not os.access(_SDKMANAGER_TOOL, os.X_OK):
+ logging.warning('Downloading Android SDK to %s',
+ _STARBOARD_TOOLCHAINS_SDK_DIR)
+ if os.path.exists(_STARBOARD_TOOLCHAINS_SDK_DIR):
+ shutil.rmtree(_STARBOARD_TOOLCHAINS_SDK_DIR)
+ _DownloadAndUnzipFile(_SDK_URL, _STARBOARD_TOOLCHAINS_SDK_DIR)
+ if not os.access(_SDKMANAGER_TOOL, os.X_OK):
+ logging.error('SDK download failed.')
+ sys.exit(1)
+
+ # Run the "sdkmanager" command with the appropriate packages
+
+ if _IsOnBuildbot():
+ stdin = subprocess.PIPE
+ else:
+ stdin = sys.stdin
+
+ p = subprocess.Popen(
+ [_SDKMANAGER_TOOL, '--verbose'] + _ANDROID_SDK_PACKAGES, stdin=stdin)
+
+ if _IsOnBuildbot():
+ time.sleep(_SDK_LICENSE_PROMPT_SLEEP_SECONDS)
+ try:
+ p.stdin.write('y\n')
+ except IOError:
+ logging.warning('There were no SDK licenses to accept.')
+
+ p.wait()
+
+
+def _MaybeMakeToolchain(abi):
+ """Run the NDK's make_standalone_toolchain.py if necessary."""
+ tools_arch = _TOOLS_ABI_ARCH_MAP[abi]
+ tools_path = GetToolsPath(abi)
+ if _CheckStamp(tools_path):
+ logging.info('NDK %s toolchain already at %s', tools_arch,
+ _GetInstalledNdkRevision())
+ return
+
+ logging.warning('Installing NDK %s toolchain %s in %s', tools_arch,
+ _GetInstalledNdkRevision(), tools_path)
+
+ if os.path.exists(tools_path):
+ shutil.rmtree(tools_path)
+
+ # Run the NDK script to make the standalone toolchain
+ script_path = os.path.join(GetNdkPath(), 'build', 'tools',
+ 'make_standalone_toolchain.py')
+ args = [
+ script_path, '--arch', tools_arch, '--api', _ANDROID_NDK_API_LEVEL,
+ '--stl', 'libc++', '--install-dir', tools_path
+ ]
+ script_proc = subprocess.Popen(args)
+ rc = script_proc.wait()
+ if rc != 0:
+ raise RuntimeError('%s failed.' % script_path)
+
+ _UpdateStamp(tools_path)
diff --git a/src/starboard/android/shared/socket_get_interface_address.cc b/src/starboard/android/shared/socket_get_interface_address.cc
new file mode 100644
index 0000000..fe75115
--- /dev/null
+++ b/src/starboard/android/shared/socket_get_interface_address.cc
@@ -0,0 +1,93 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "net/base/net_util.h"
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/log.h"
+#include "starboard/memory.h"
+#include "starboard/socket.h"
+
+using starboard::android::shared::JniEnvExt;
+
+namespace {
+
+const size_t kDefaultPrefixLength = 8;
+
+} // namespace
+
+// Note: The following is an incorrect implementation for
+// SbSocketGetInterfaceAddress. Specifically, things missing are:
+// We should see if the destination is NULL, or ANY address, or a regular
+// unicast address.
+// Also, currently, we assume netmask is 255.255.255.0 in IPv4, and
+// has a prefix length of 64 for IPv6.
+// For IPv6, there are a few rules about which IPs are valid (and prefered).
+// E.g. globally routable IP addresses are prefered over ULAs.
+bool SbSocketGetInterfaceAddress(const SbSocketAddress* const /*destination*/,
+ SbSocketAddress* out_source_address,
+ SbSocketAddress* out_netmask) {
+ if (out_source_address == NULL) {
+ return false;
+ }
+
+ SbMemorySet(out_source_address->address, 0,
+ sizeof(out_source_address->address));
+ out_source_address->port = 0;
+
+ JniEnvExt* env = JniEnvExt::Get();
+
+ jbyteArray s = (jbyteArray)env->CallStarboardObjectMethodOrAbort(
+ "getLocalInterfaceAddress", "()[B");
+ if (s == NULL) {
+ return false;
+ }
+
+ jint sz = env->GetArrayLength(s);
+ if (sz > sizeof(out_source_address->address)) {
+ // This should never happen
+ SB_LOG(ERROR) << "SbSocketGetInterfaceAddress address too big";
+ return false;
+ }
+ switch (sz) {
+ case 4:
+ out_source_address->type = kSbSocketAddressTypeIpv4;
+ if (out_netmask) {
+ out_netmask->address[0] = 255;
+ out_netmask->address[1] = 255;
+ out_netmask->address[2] = 255;
+ out_netmask->address[3] = 0;
+ out_netmask->type = kSbSocketAddressTypeIpv4;
+ }
+ break;
+ default:
+ out_source_address->type = kSbSocketAddressTypeIpv6;
+ if (out_netmask) {
+ for (int i = 0; i < kDefaultPrefixLength; ++i) {
+ out_netmask->address[i] = 0xff;
+ }
+ for (int i = kDefaultPrefixLength; i < net::kIPv6AddressSize; ++i) {
+ out_netmask->address[i] = 0;
+ }
+ out_netmask->type = kSbSocketAddressTypeIpv6;
+ }
+ break;
+ }
+
+ jbyte* bytes = env->GetByteArrayElements(s, NULL);
+ SB_CHECK(bytes) << "GetByteArrayElements failed";
+ SbMemoryCopy(out_source_address->address, bytes, sz);
+ env->ReleaseByteArrayElements(s, bytes, JNI_ABORT);
+
+ return true;
+}
diff --git a/src/starboard/android/shared/speech_recognizer_impl.cc b/src/starboard/android/shared/speech_recognizer_impl.cc
new file mode 100644
index 0000000..bea9e52
--- /dev/null
+++ b/src/starboard/android/shared/speech_recognizer_impl.cc
@@ -0,0 +1,331 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/shared/starboard/speech_recognizer/speech_recognizer_internal.h"
+
+#include <android/native_activity.h>
+#include <jni.h>
+
+#include <limits>
+#include <vector>
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/mutex.h"
+#include "starboard/shared/starboard/thread_checker.h"
+#include "starboard/string.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+namespace {
+// Android speech recognizer error.
+// Reference:
+// https://developer.android.com/reference/android/speech/SpeechRecognizer.html
+enum SpeechRecognizerErrorCode {
+ kErrorNetworkTimeout = 1,
+ kErrorNetwork = 2,
+ kErrorAudio = 3,
+ kErrorService = 4,
+ kErrorClient = 5,
+ kErrorSpeechTimeout = 6,
+ kErrorNoMatch = 7,
+ kErrorRecognizerBusy = 8,
+ kErrorInsufficientPermissions = 9,
+};
+} // namespace
+
+class SbSpeechRecognizerImpl : public SbSpeechRecognizerPrivate {
+ public:
+ explicit SbSpeechRecognizerImpl(const SbSpeechRecognizerHandler* handler);
+ ~SbSpeechRecognizerImpl();
+
+ // Called from constructor's thread.
+ bool Start(const SbSpeechConfiguration* configuration) override;
+ void Stop() override;
+ void Cancel() override;
+
+ // Called from Android's UI thread.
+ void OnSpeechDetected(bool detected);
+ void OnError(int error);
+ void OnResults(const std::vector<std::string>& results,
+ const std::vector<float>& confidences,
+ bool is_final);
+
+ private:
+ SbSpeechRecognizerHandler handler_;
+ jobject j_voice_recognizer_;
+ bool is_started_;
+
+ ::starboard::shared::starboard::ThreadChecker thread_checker_;
+};
+
+SbSpeechRecognizerImpl::SbSpeechRecognizerImpl(
+ const SbSpeechRecognizerHandler* handler)
+ : handler_(*handler), is_started_(false) {
+ JniEnvExt* env = JniEnvExt::Get();
+ jobject local_ref = env->CallStarboardObjectMethodOrAbort(
+ "getVoiceRecognizer", "()Ldev/cobalt/coat/VoiceRecognizer;");
+ j_voice_recognizer_ = env->ConvertLocalRefToGlobalRef(local_ref);
+}
+
+SbSpeechRecognizerImpl::~SbSpeechRecognizerImpl() {
+ SB_DCHECK(thread_checker_.CalledOnValidThread());
+
+ Cancel();
+
+ JniEnvExt* env = JniEnvExt::Get();
+ env->DeleteGlobalRef(j_voice_recognizer_);
+}
+
+bool SbSpeechRecognizerImpl::Start(const SbSpeechConfiguration* configuration) {
+ SB_DCHECK(thread_checker_.CalledOnValidThread());
+ SB_DCHECK(configuration);
+
+ if (is_started_) {
+ return false;
+ }
+
+ JniEnvExt* env = JniEnvExt::Get();
+ env->CallVoidMethodOrAbort(
+ j_voice_recognizer_, "startRecognition", "(ZZIJ)V",
+ configuration->continuous, configuration->interim_results,
+ configuration->max_alternatives, reinterpret_cast<jlong>(this));
+
+ is_started_ = true;
+ return true;
+}
+
+void SbSpeechRecognizerImpl::Stop() {
+ SB_DCHECK(thread_checker_.CalledOnValidThread());
+
+ if (!is_started_) {
+ return;
+ }
+
+ JniEnvExt* env = JniEnvExt::Get();
+ env->CallVoidMethodOrAbort(j_voice_recognizer_, "stopRecognition", "()V");
+
+ is_started_ = false;
+}
+
+void SbSpeechRecognizerImpl::Cancel() {
+ Stop();
+}
+
+void SbSpeechRecognizerImpl::OnSpeechDetected(bool detected) {
+ // Called from Android's UI thread instead of constructor's thread.
+ SB_DCHECK(!thread_checker_.CalledOnValidThread());
+
+ handler_.on_speech_detected(handler_.context, detected);
+}
+
+void SbSpeechRecognizerImpl::OnError(int error) {
+ // Called from Android's UI thread instead of constructor's thread.
+ SB_DCHECK(!thread_checker_.CalledOnValidThread());
+
+ SbSpeechRecognizerError recognizer_error;
+ switch (error) {
+ case kErrorNetworkTimeout:
+ recognizer_error = kSbNetworkError;
+ break;
+ case kErrorNetwork:
+ recognizer_error = kSbNetworkError;
+ break;
+ case kErrorAudio:
+ recognizer_error = kSbAudioCaptureError;
+ break;
+ case kErrorService:
+ recognizer_error = kSbNetworkError;
+ break;
+ case kErrorClient:
+ recognizer_error = kSbAborted;
+ break;
+ case kErrorSpeechTimeout:
+ recognizer_error = kSbNoSpeechError;
+ break;
+ case kErrorRecognizerBusy:
+ case kErrorInsufficientPermissions:
+ recognizer_error = kSbNotAllowed;
+ break;
+ case kErrorNoMatch:
+ // Maybe keep listening until found a match or time-out triggered by
+ // client.
+ recognizer_error = kSbNoSpeechError;
+ break;
+ }
+ handler_.on_error(handler_.context, recognizer_error);
+}
+
+void SbSpeechRecognizerImpl::OnResults(const std::vector<std::string>& results,
+ const std::vector<float>& confidences,
+ bool is_final) {
+ // Called from Android's UI thread instead of constructor's thread.
+ SB_DCHECK(!thread_checker_.CalledOnValidThread());
+
+ bool has_confidence = (confidences.size() != 0);
+ if (has_confidence) {
+ SB_DCHECK(confidences.size() == results.size());
+ }
+ int kSpeechResultSize = results.size();
+ std::vector<SbSpeechResult> speech_results(kSpeechResultSize);
+ for (int i = 0; i < kSpeechResultSize; ++i) {
+ // The callback is responsible for freeing the buffer with
+ // SbMemoryDeallocate.
+ speech_results[i].transcript = SbStringDuplicate(results[i].c_str());
+ speech_results[i].confidence =
+ has_confidence ? confidences[i]
+ : std::numeric_limits<float>::quiet_NaN();
+ }
+ handler_.on_results(handler_.context, speech_results.data(),
+ kSpeechResultSize, is_final);
+}
+
+} // namespace shared
+} // namespace android
+} // namespace starboard
+
+namespace {
+
+using starboard::android::shared::JniEnvExt;
+
+starboard::Mutex s_speech_recognizer_mutex_;
+SbSpeechRecognizer s_speech_recognizer = kSbSpeechRecognizerInvalid;
+} // namespace
+
+// static
+SbSpeechRecognizer SbSpeechRecognizerPrivate::CreateSpeechRecognizer(
+ const SbSpeechRecognizerHandler* handler) {
+ starboard::ScopedLock lock(s_speech_recognizer_mutex_);
+
+ SB_DCHECK(!SbSpeechRecognizerIsValid(s_speech_recognizer));
+ s_speech_recognizer =
+ new starboard::android::shared::SbSpeechRecognizerImpl(handler);
+ return s_speech_recognizer;
+}
+
+// static
+void SbSpeechRecognizerPrivate::DestroySpeechRecognizer(
+ SbSpeechRecognizer speech_recognizer) {
+ starboard::ScopedLock lock(s_speech_recognizer_mutex_);
+
+ SB_DCHECK(s_speech_recognizer == speech_recognizer);
+ SB_DCHECK(SbSpeechRecognizerIsValid(s_speech_recognizer));
+ delete s_speech_recognizer;
+ s_speech_recognizer = kSbSpeechRecognizerInvalid;
+}
+
+extern "C" SB_EXPORT_PLATFORM void
+Java_dev_cobalt_coat_VoiceRecognizer_nativeOnSpeechDetected(
+ JNIEnv* env,
+ jobject jcaller,
+ jlong nativeSpeechRecognizerImpl,
+ jboolean detected) {
+ starboard::ScopedLock lock(s_speech_recognizer_mutex_);
+
+ starboard::android::shared::SbSpeechRecognizerImpl* native =
+ reinterpret_cast<starboard::android::shared::SbSpeechRecognizerImpl*>(
+ nativeSpeechRecognizerImpl);
+ // This is called by the Android UI thread and it is possible that the
+ // SbSpeechRecognizer is destroyed before this is called.
+ if (native != s_speech_recognizer) {
+ SB_DLOG(WARNING) << "The speech recognizer is destroyed.";
+ return;
+ }
+
+ native->OnSpeechDetected(detected);
+}
+
+extern "C" SB_EXPORT_PLATFORM void
+Java_dev_cobalt_coat_VoiceRecognizer_nativeOnError(
+ JNIEnv* env,
+ jobject jcaller,
+ jlong nativeSpeechRecognizerImpl,
+ jint error) {
+ starboard::ScopedLock lock(s_speech_recognizer_mutex_);
+
+ starboard::android::shared::SbSpeechRecognizerImpl* native =
+ reinterpret_cast<starboard::android::shared::SbSpeechRecognizerImpl*>(
+ nativeSpeechRecognizerImpl);
+ // This is called by the Android UI thread and it is possible that the
+ // SbSpeechRecognizer is destroyed before this is called.
+ if (native != s_speech_recognizer) {
+ SB_DLOG(WARNING) << "The speech recognizer is destroyed.";
+ return;
+ }
+
+ native->OnError(error);
+}
+
+extern "C" SB_EXPORT_PLATFORM void
+Java_dev_cobalt_coat_VoiceRecognizer_nativeHandlePermission(
+ JNIEnv* env,
+ jobject jcaller,
+ jlong nativeSpeechRecognizerImpl,
+ jboolean is_granted) {
+ starboard::ScopedLock lock(s_speech_recognizer_mutex_);
+
+ starboard::android::shared::SbSpeechRecognizerImpl* native =
+ reinterpret_cast<starboard::android::shared::SbSpeechRecognizerImpl*>(
+ nativeSpeechRecognizerImpl);
+ // This is called by the Android UI thread and it is possible that the
+ // SbSpeechRecognizer is destroyed before this is called.
+ if (native != s_speech_recognizer) {
+ SB_DLOG(WARNING) << "The speech recognizer is destroyed.";
+ return;
+ }
+ if (!is_granted) {
+ native->OnError(starboard::android::shared::kErrorInsufficientPermissions);
+ }
+}
+
+extern "C" SB_EXPORT_PLATFORM void
+Java_dev_cobalt_coat_VoiceRecognizer_nativeOnResults(
+ JniEnvExt* env,
+ jobject unused_this,
+ jlong nativeSpeechRecognizerImpl,
+ jobjectArray results,
+ jfloatArray confidences,
+ jboolean is_final) {
+ starboard::ScopedLock lock(s_speech_recognizer_mutex_);
+
+ starboard::android::shared::SbSpeechRecognizerImpl* native =
+ reinterpret_cast<starboard::android::shared::SbSpeechRecognizerImpl*>(
+ nativeSpeechRecognizerImpl);
+ // This is called by the Android UI thread and it is possible that the
+ // SbSpeechRecognizer is destroyed before this is called.
+ if (native != s_speech_recognizer) {
+ SB_DLOG(WARNING) << "The speech recognizer is destroyed.";
+ return;
+ }
+
+ std::vector<std::string> options;
+ jint argc = env->GetArrayLength(results);
+ for (jint i = 0; i < argc; i++) {
+ starboard::android::shared::ScopedLocalJavaRef<jstring> element(
+ env->GetObjectArrayElement(results, i));
+ std::string utf_str = env->GetStringStandardUTFOrAbort(element.Get());
+ options.push_back(utf_str);
+ }
+
+ std::vector<float> scores(options.size(), 0.0);
+ if (confidences != NULL) {
+ SB_DCHECK(argc == env->GetArrayLength(results));
+ float* confidences_array = env->GetFloatArrayElements(confidences, NULL);
+ std::copy(confidences_array, confidences_array + argc, scores.begin());
+ env->ReleaseFloatArrayElements(confidences, confidences_array, 0);
+ }
+ native->OnResults(options, scores, is_final);
+}
diff --git a/src/starboard/android/shared/speech_synthesis_cancel.cc b/src/starboard/android/shared/speech_synthesis_cancel.cc
new file mode 100644
index 0000000..6eab70e
--- /dev/null
+++ b/src/starboard/android/shared/speech_synthesis_cancel.cc
@@ -0,0 +1,31 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/speech_synthesis.h"
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+
+using starboard::android::shared::JniEnvExt;
+using starboard::android::shared::ScopedLocalJavaRef;
+
+void SbSpeechSynthesisCancel() {
+ JniEnvExt* env = JniEnvExt::Get();
+
+ ScopedLocalJavaRef<jobject> j_tts_helper(
+ env->CallStarboardObjectMethodOrAbort(
+ "getTextToSpeechHelper",
+ "()Ldev/cobalt/coat/CobaltTextToSpeechHelper;"));
+ env->CallVoidMethodOrAbort(j_tts_helper.Get(), "cancel", "()V");
+}
diff --git a/src/starboard/android/shared/speech_synthesis_speak.cc b/src/starboard/android/shared/speech_synthesis_speak.cc
new file mode 100644
index 0000000..b571951
--- /dev/null
+++ b/src/starboard/android/shared/speech_synthesis_speak.cc
@@ -0,0 +1,33 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/speech_synthesis.h"
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+
+using starboard::android::shared::JniEnvExt;
+using starboard::android::shared::ScopedLocalJavaRef;
+
+void SbSpeechSynthesisSpeak(const char* text) {
+ JniEnvExt* env = JniEnvExt::Get();
+ ScopedLocalJavaRef<jobject> j_tts_helper(
+ env->CallStarboardObjectMethodOrAbort(
+ "getTextToSpeechHelper",
+ "()Ldev/cobalt/coat/CobaltTextToSpeechHelper;"));
+ ScopedLocalJavaRef<jstring> j_text_string(
+ env->NewStringStandardUTFOrAbort(text));
+ env->CallVoidMethodOrAbort(j_tts_helper.Get(),
+ "speak", "(Ljava/lang/String;)V", j_text_string.Get());
+}
diff --git a/src/starboard/android/shared/starboard_platform.gypi b/src/starboard/android/shared/starboard_platform.gypi
new file mode 100644
index 0000000..64bcca9
--- /dev/null
+++ b/src/starboard/android/shared/starboard_platform.gypi
@@ -0,0 +1,486 @@
+# Copyright 2016 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+{
+ 'variables': {
+ 'has_input_events_filter' : '<!(python ../../../build/file_exists.py <(DEPTH)/starboard/android/shared/input_events_filter.cc)',
+ },
+ 'includes': [
+ '<(DEPTH)/starboard/shared/starboard/player/filter/player_filter.gypi',
+ ],
+ 'targets': [
+ {
+ 'target_name': 'starboard_base_symbolize',
+ 'type': 'static_library',
+ 'sources': [
+ '<(DEPTH)/base/third_party/symbolize/demangle.cc',
+ '<(DEPTH)/base/third_party/symbolize/symbolize.cc',
+ ],
+ },
+ # Copy sources that we compile from the NDK so that we can reference them
+ # by a relative path. Otherwise, without GYP pathname relativization
+ # different configuration builds would clobber each other since they'd all
+ # generate their .o at the same path in the NDK sources directory.
+ {
+ 'target_name': 'ndk_sources',
+ 'type': 'none',
+ 'copies': [{
+ 'destination': '<(SHARED_INTERMEDIATE_DIR)/ndk-sources/',
+ 'files': [
+ '<(NDK_HOME)/sources/android/cpufeatures/cpu-features.c',
+ ],
+ }],
+ },
+ {
+ 'target_name': 'starboard_platform',
+ 'type': 'static_library',
+ 'sources': [
+ '<@(filter_based_player_sources)',
+ 'accessibility_get_caption_settings.cc',
+ 'accessibility_get_display_settings.cc',
+ 'accessibility_get_text_to_speech_settings.cc',
+ 'accessibility_set_captions_enabled.cc',
+ 'android_main.cc',
+ 'application_android.cc',
+ 'application_android.h',
+ 'atomic_public.h',
+ 'audio_decoder.cc',
+ 'audio_decoder.h',
+ 'audio_renderer.h',
+ 'audio_sink_get_max_channels.cc',
+ 'audio_sink_get_nearest_supported_sample_frequency.cc',
+ 'audio_sink_is_audio_frame_storage_type_supported.cc',
+ 'audio_sink_is_audio_sample_type_supported.cc',
+ 'audio_track_audio_sink_type.cc',
+ 'audio_track_audio_sink_type.h',
+ 'configuration_public.h',
+ 'decode_target_create.cc',
+ 'decode_target_create.h',
+ 'decode_target_get_info.cc',
+ 'decode_target_internal.cc',
+ 'decode_target_internal.h',
+ 'decode_target_release.cc',
+ 'directory_close.cc',
+ 'directory_get_next.cc',
+ 'directory_internal.h',
+ 'directory_open.cc',
+ 'drm_create_system.cc',
+ 'drm_system.cc',
+ 'drm_system.h',
+ 'egl_swap_buffers.cc',
+ 'file_can_open.cc',
+ 'file_close.cc',
+ 'file_delete.cc',
+ 'file_exists.cc',
+ 'file_flush.cc',
+ 'file_get_info.cc',
+ 'file_get_path_info.cc',
+ 'file_internal.cc',
+ 'file_internal.h',
+ 'file_open.cc',
+ 'file_read.cc',
+ 'file_seek.cc',
+ 'file_truncate.cc',
+ 'file_write.cc',
+ 'get_home_directory.cc',
+ 'input_events_generator.cc',
+ 'input_events_generator.h',
+ 'jni_env_ext.cc',
+ 'jni_env_ext.h',
+ 'jni_utils.h',
+ 'log.cc',
+ 'log_flush.cc',
+ 'log_format.cc',
+ 'log_internal.h',
+ 'log_is_tty.cc',
+ 'log_raw.cc',
+ 'main.cc',
+ 'media_codec_bridge.cc',
+ 'media_codec_bridge.h',
+ 'media_common.h',
+ 'media_decoder.cc',
+ 'media_decoder.h',
+ 'media_get_audio_configuration.cc',
+ 'media_get_audio_output_count.cc',
+ 'media_get_initial_buffer_capacity.cc',
+ 'media_get_max_buffer_capacity.cc',
+ 'media_is_audio_supported.cc',
+ 'media_is_output_protected.cc',
+ 'media_is_supported.cc',
+ 'media_is_video_supported.cc',
+ 'media_set_output_protection.cc',
+ 'microphone_impl.cc',
+ 'player_components_impl.cc',
+ 'player_create.cc',
+ 'player_destroy.cc',
+ 'player_set_bounds.cc',
+ 'player_set_playback_rate.cc',
+ 'sanitizer_options.cc',
+ 'socket_get_interface_address.cc',
+ 'speech_recognizer_impl.cc',
+ 'speech_synthesis_cancel.cc',
+ 'speech_synthesis_speak.cc',
+ 'system_get_connection_type.cc',
+ 'system_get_device_type.cc',
+ 'system_get_locale_id.cc',
+ 'system_get_path.cc',
+ 'system_get_property.cc',
+ 'system_get_stack.cc',
+ 'system_has_capability.cc',
+ 'system_platform_error.cc',
+ 'system_request_stop.cc',
+ 'system_request_suspend.cc',
+ 'thread_create.cc',
+ 'thread_create_priority.cc',
+ 'thread_get_name.cc',
+ 'thread_types_public.h',
+ 'time_zone_get_dst_name.cc',
+ 'time_zone_get_name.cc',
+ 'trace_util.h',
+ 'video_decoder.cc',
+ 'video_decoder.h',
+ 'video_render_algorithm.cc',
+ 'video_render_algorithm.h',
+ 'video_window.cc',
+ 'video_window.h',
+ 'window_create.cc',
+ 'window_destroy.cc',
+ 'window_get_platform_handle.cc',
+ 'window_get_size.cc',
+ 'window_internal.h',
+ '<(SHARED_INTERMEDIATE_DIR)/ndk-sources/cpu-features.c',
+ '<(DEPTH)/starboard/accessibility.h',
+ '<(DEPTH)/starboard/atomic.h',
+ '<(DEPTH)/starboard/audio_sink.h',
+ '<(DEPTH)/starboard/common/ref_counted.h',
+ '<(DEPTH)/starboard/common/scoped_ptr.h',
+ '<(DEPTH)/starboard/condition_variable.h',
+ '<(DEPTH)/starboard/configuration.h',
+ '<(DEPTH)/starboard/decode_target.h',
+ '<(DEPTH)/starboard/directory.h',
+ '<(DEPTH)/starboard/drm.h',
+ '<(DEPTH)/starboard/event.h',
+ '<(DEPTH)/starboard/export.h',
+ '<(DEPTH)/starboard/file.h',
+ '<(DEPTH)/starboard/input.h',
+ '<(DEPTH)/starboard/key.h',
+ '<(DEPTH)/starboard/log.h',
+ '<(DEPTH)/starboard/media.h',
+ '<(DEPTH)/starboard/memory.h',
+ '<(DEPTH)/starboard/mutex.h',
+ '<(DEPTH)/starboard/once.h',
+ '<(DEPTH)/starboard/player.h',
+ '<(DEPTH)/starboard/queue.h',
+ '<(DEPTH)/starboard/socket.h',
+ '<(DEPTH)/starboard/speech_synthesis.h',
+ '<(DEPTH)/starboard/string.h',
+ '<(DEPTH)/starboard/system.h',
+ '<(DEPTH)/starboard/thread.h',
+ '<(DEPTH)/starboard/time_zone.h',
+ '<(DEPTH)/starboard/types.h',
+ '<(DEPTH)/starboard/window.h',
+ '<(DEPTH)/starboard/android/shared/window_blur_on_screen_keyboard.cc',
+ '<(DEPTH)/starboard/android/shared//window_focus_on_screen_keyboard.cc',
+ '<(DEPTH)/starboard/android/shared/window_get_on_screen_keyboard_bounding_rect.cc',
+ '<(DEPTH)/starboard/android/shared/window_hide_on_screen_keyboard.cc',
+ '<(DEPTH)/starboard/android/shared/window_is_on_screen_keyboard_shown.cc',
+ '<(DEPTH)/starboard/android/shared/window_set_on_screen_keyboard_keep_focus.cc',
+ '<(DEPTH)/starboard/android/shared/window_show_on_screen_keyboard.cc',
+ '<(DEPTH)/starboard/shared/dlmalloc/memory_map.cc',
+ '<(DEPTH)/starboard/shared/dlmalloc/memory_protect.cc',
+ '<(DEPTH)/starboard/shared/dlmalloc/memory_unmap.cc',
+ '<(DEPTH)/starboard/shared/gcc/atomic_gcc_public.h',
+ '<(DEPTH)/starboard/shared/gles/gl_call.h',
+ '<(DEPTH)/starboard/shared/internal_only.h',
+ '<(DEPTH)/starboard/shared/iso/character_is_alphanumeric.cc',
+ '<(DEPTH)/starboard/shared/iso/character_is_digit.cc',
+ '<(DEPTH)/starboard/shared/iso/character_is_hex_digit.cc',
+ '<(DEPTH)/starboard/shared/iso/character_is_space.cc',
+ '<(DEPTH)/starboard/shared/iso/character_is_upper.cc',
+ '<(DEPTH)/starboard/shared/iso/character_to_lower.cc',
+ '<(DEPTH)/starboard/shared/iso/character_to_upper.cc',
+ '<(DEPTH)/starboard/shared/iso/double_absolute.cc',
+ '<(DEPTH)/starboard/shared/iso/double_exponent.cc',
+ '<(DEPTH)/starboard/shared/iso/double_floor.cc',
+ '<(DEPTH)/starboard/shared/iso/double_is_finite.cc',
+ '<(DEPTH)/starboard/shared/iso/double_is_nan.cc',
+ '<(DEPTH)/starboard/shared/iso/impl/directory_close.h',
+ '<(DEPTH)/starboard/shared/iso/impl/directory_get_next.h',
+ '<(DEPTH)/starboard/shared/iso/impl/directory_open.h',
+ '<(DEPTH)/starboard/shared/iso/memory_allocate_unchecked.cc',
+ '<(DEPTH)/starboard/shared/iso/memory_compare.cc',
+ '<(DEPTH)/starboard/shared/iso/memory_copy.cc',
+ '<(DEPTH)/starboard/shared/iso/memory_find_byte.cc',
+ '<(DEPTH)/starboard/shared/iso/memory_free.cc',
+ '<(DEPTH)/starboard/shared/iso/memory_move.cc',
+ '<(DEPTH)/starboard/shared/iso/memory_reallocate_unchecked.cc',
+ '<(DEPTH)/starboard/shared/iso/memory_set.cc',
+ '<(DEPTH)/starboard/shared/iso/string_compare.cc',
+ '<(DEPTH)/starboard/shared/iso/string_compare_all.cc',
+ '<(DEPTH)/starboard/shared/iso/string_find_character.cc',
+ '<(DEPTH)/starboard/shared/iso/string_find_last_character.cc',
+ '<(DEPTH)/starboard/shared/iso/string_find_string.cc',
+ '<(DEPTH)/starboard/shared/iso/string_get_length.cc',
+ '<(DEPTH)/starboard/shared/iso/string_get_length_wide.cc',
+ '<(DEPTH)/starboard/shared/iso/string_parse_double.cc',
+ '<(DEPTH)/starboard/shared/iso/string_parse_signed_integer.cc',
+ '<(DEPTH)/starboard/shared/iso/string_parse_uint64.cc',
+ '<(DEPTH)/starboard/shared/iso/string_parse_unsigned_integer.cc',
+ '<(DEPTH)/starboard/shared/iso/string_scan.cc',
+ '<(DEPTH)/starboard/shared/iso/system_binary_search.cc',
+ '<(DEPTH)/starboard/shared/iso/system_sort.cc',
+ '<(DEPTH)/starboard/shared/libevent/socket_waiter_add.cc',
+ '<(DEPTH)/starboard/shared/libevent/socket_waiter_create.cc',
+ '<(DEPTH)/starboard/shared/libevent/socket_waiter_destroy.cc',
+ '<(DEPTH)/starboard/shared/libevent/socket_waiter_internal.cc',
+ '<(DEPTH)/starboard/shared/libevent/socket_waiter_remove.cc',
+ '<(DEPTH)/starboard/shared/libevent/socket_waiter_wait.cc',
+ '<(DEPTH)/starboard/shared/libevent/socket_waiter_wait_timed.cc',
+ '<(DEPTH)/starboard/shared/libevent/socket_waiter_wake_up.cc',
+ '<(DEPTH)/starboard/shared/linux/byte_swap.cc',
+ '<(DEPTH)/starboard/shared/linux/memory_get_stack_bounds.cc',
+ '<(DEPTH)/starboard/shared/linux/page_internal.cc',
+ '<(DEPTH)/starboard/shared/linux/system_get_random_data.cc',
+ '<(DEPTH)/starboard/shared/linux/system_get_total_cpu_memory.cc',
+ '<(DEPTH)/starboard/shared/linux/system_get_used_cpu_memory.cc',
+ '<(DEPTH)/starboard/shared/linux/system_is_debugger_attached.cc',
+ '<(DEPTH)/starboard/shared/linux/system_symbolize.cc',
+ '<(DEPTH)/starboard/shared/linux/thread_get_id.cc',
+ '<(DEPTH)/starboard/shared/linux/thread_set_name.cc',
+ '<(DEPTH)/starboard/shared/nouser/user_get_current.cc',
+ '<(DEPTH)/starboard/shared/nouser/user_get_property.cc',
+ '<(DEPTH)/starboard/shared/nouser/user_get_signed_in.cc',
+ '<(DEPTH)/starboard/shared/nouser/user_internal.cc',
+ '<(DEPTH)/starboard/shared/nouser/user_internal.h',
+ '<(DEPTH)/starboard/shared/posix/directory_create.cc',
+ '<(DEPTH)/starboard/shared/posix/impl/file_can_open.h',
+ '<(DEPTH)/starboard/shared/posix/impl/file_close.h',
+ '<(DEPTH)/starboard/shared/posix/impl/file_delete.h',
+ '<(DEPTH)/starboard/shared/posix/impl/file_flush.h',
+ '<(DEPTH)/starboard/shared/posix/impl/file_get_info.h',
+ '<(DEPTH)/starboard/shared/posix/impl/file_get_path_info.h',
+ '<(DEPTH)/starboard/shared/posix/impl/file_open.h',
+ '<(DEPTH)/starboard/shared/posix/impl/file_read.h',
+ '<(DEPTH)/starboard/shared/posix/impl/file_seek.h',
+ '<(DEPTH)/starboard/shared/posix/impl/file_truncate.h',
+ '<(DEPTH)/starboard/shared/posix/impl/file_write.h',
+ '<(DEPTH)/starboard/shared/posix/memory_allocate_aligned_unchecked.cc',
+ '<(DEPTH)/starboard/shared/posix/memory_flush.cc',
+ '<(DEPTH)/starboard/shared/posix/memory_free_aligned.cc',
+ '<(DEPTH)/starboard/shared/posix/set_non_blocking_internal.cc',
+ '<(DEPTH)/starboard/shared/posix/socket_accept.cc',
+ '<(DEPTH)/starboard/shared/posix/socket_bind.cc',
+ '<(DEPTH)/starboard/shared/posix/socket_clear_last_error.cc',
+ '<(DEPTH)/starboard/shared/posix/socket_connect.cc',
+ '<(DEPTH)/starboard/shared/posix/socket_create.cc',
+ '<(DEPTH)/starboard/shared/posix/socket_destroy.cc',
+ '<(DEPTH)/starboard/shared/posix/socket_free_resolution.cc',
+ '<(DEPTH)/starboard/shared/posix/socket_get_last_error.cc',
+ '<(DEPTH)/starboard/shared/posix/socket_get_local_address.cc',
+ '<(DEPTH)/starboard/shared/posix/socket_internal.cc',
+ '<(DEPTH)/starboard/shared/posix/socket_is_connected.cc',
+ '<(DEPTH)/starboard/shared/posix/socket_is_connected_and_idle.cc',
+ '<(DEPTH)/starboard/shared/posix/socket_join_multicast_group.cc',
+ '<(DEPTH)/starboard/shared/posix/socket_listen.cc',
+ '<(DEPTH)/starboard/shared/posix/socket_receive_from.cc',
+ '<(DEPTH)/starboard/shared/posix/socket_resolve.cc',
+ '<(DEPTH)/starboard/shared/posix/socket_send_to.cc',
+ '<(DEPTH)/starboard/shared/posix/socket_set_broadcast.cc',
+ '<(DEPTH)/starboard/shared/posix/socket_set_receive_buffer_size.cc',
+ '<(DEPTH)/starboard/shared/posix/socket_set_reuse_address.cc',
+ '<(DEPTH)/starboard/shared/posix/socket_set_send_buffer_size.cc',
+ '<(DEPTH)/starboard/shared/posix/socket_set_tcp_keep_alive.cc',
+ '<(DEPTH)/starboard/shared/posix/socket_set_tcp_no_delay.cc',
+ '<(DEPTH)/starboard/shared/posix/socket_set_tcp_window_scaling.cc',
+ '<(DEPTH)/starboard/shared/posix/storage_write_record.cc',
+ '<(DEPTH)/starboard/shared/posix/string_compare_no_case.cc',
+ '<(DEPTH)/starboard/shared/posix/string_compare_no_case_n.cc',
+ '<(DEPTH)/starboard/shared/posix/string_compare_wide.cc',
+ '<(DEPTH)/starboard/shared/posix/string_format.cc',
+ '<(DEPTH)/starboard/shared/posix/string_format_wide.cc',
+ '<(DEPTH)/starboard/shared/posix/system_break_into_debugger.cc',
+ '<(DEPTH)/starboard/shared/posix/system_clear_last_error.cc',
+ '<(DEPTH)/starboard/shared/posix/system_get_error_string.cc',
+ '<(DEPTH)/starboard/shared/posix/system_get_last_error.cc',
+ '<(DEPTH)/starboard/shared/posix/system_get_number_of_processors.cc',
+ '<(DEPTH)/starboard/shared/posix/thread_sleep.cc',
+ '<(DEPTH)/starboard/shared/posix/time_get_monotonic_now.cc',
+ '<(DEPTH)/starboard/shared/posix/time_get_monotonic_thread_now.cc',
+ '<(DEPTH)/starboard/shared/posix/time_get_now.cc',
+ '<(DEPTH)/starboard/shared/posix/time_zone_get_current.cc',
+ '<(DEPTH)/starboard/shared/pthread/condition_variable_broadcast.cc',
+ '<(DEPTH)/starboard/shared/pthread/condition_variable_create.cc',
+ '<(DEPTH)/starboard/shared/pthread/condition_variable_destroy.cc',
+ '<(DEPTH)/starboard/shared/pthread/condition_variable_signal.cc',
+ '<(DEPTH)/starboard/shared/pthread/condition_variable_wait.cc',
+ '<(DEPTH)/starboard/shared/pthread/condition_variable_wait_timed.cc',
+ '<(DEPTH)/starboard/shared/pthread/mutex_acquire.cc',
+ '<(DEPTH)/starboard/shared/pthread/mutex_acquire_try.cc',
+ '<(DEPTH)/starboard/shared/pthread/mutex_create.cc',
+ '<(DEPTH)/starboard/shared/pthread/mutex_destroy.cc',
+ '<(DEPTH)/starboard/shared/pthread/mutex_release.cc',
+ '<(DEPTH)/starboard/shared/pthread/once.cc',
+ '<(DEPTH)/starboard/shared/pthread/thread_create_local_key.cc',
+ '<(DEPTH)/starboard/shared/pthread/thread_create_priority.h',
+ '<(DEPTH)/starboard/shared/pthread/thread_destroy_local_key.cc',
+ '<(DEPTH)/starboard/shared/pthread/thread_detach.cc',
+ '<(DEPTH)/starboard/shared/pthread/thread_get_current.cc',
+ '<(DEPTH)/starboard/shared/pthread/thread_get_local_value.cc',
+ '<(DEPTH)/starboard/shared/pthread/thread_is_equal.cc',
+ '<(DEPTH)/starboard/shared/pthread/thread_join.cc',
+ '<(DEPTH)/starboard/shared/pthread/thread_set_local_value.cc',
+ '<(DEPTH)/starboard/shared/pthread/thread_yield.cc',
+ '<(DEPTH)/starboard/shared/pthread/types_public.h',
+ '<(DEPTH)/starboard/shared/signal/crash_signals.h',
+ '<(DEPTH)/starboard/shared/signal/crash_signals_sigaction.cc',
+ '<(DEPTH)/starboard/shared/signal/suspend_signals.cc',
+ '<(DEPTH)/starboard/shared/signal/suspend_signals.h',
+ '<(DEPTH)/starboard/shared/starboard/application.cc',
+ '<(DEPTH)/starboard/shared/starboard/application.h',
+ '<(DEPTH)/starboard/shared/starboard/audio_sink/audio_sink_create.cc',
+ '<(DEPTH)/starboard/shared/starboard/audio_sink/audio_sink_destroy.cc',
+ '<(DEPTH)/starboard/shared/starboard/audio_sink/audio_sink_internal.cc',
+ '<(DEPTH)/starboard/shared/starboard/audio_sink/audio_sink_internal.h',
+ '<(DEPTH)/starboard/shared/starboard/audio_sink/audio_sink_is_valid.cc',
+ '<(DEPTH)/starboard/shared/starboard/audio_sink/stub_audio_sink_type.cc',
+ '<(DEPTH)/starboard/shared/starboard/audio_sink/stub_audio_sink_type.h',
+ '<(DEPTH)/starboard/shared/starboard/command_line.cc',
+ '<(DEPTH)/starboard/shared/starboard/command_line.h',
+ '<(DEPTH)/starboard/shared/starboard/directory_can_open.cc',
+ '<(DEPTH)/starboard/shared/starboard/drm/drm_close_session.cc',
+ '<(DEPTH)/starboard/shared/starboard/drm/drm_destroy_system.cc',
+ '<(DEPTH)/starboard/shared/starboard/drm/drm_generate_session_update_request.cc',
+ '<(DEPTH)/starboard/shared/starboard/drm/drm_system_internal.h',
+ '<(DEPTH)/starboard/shared/starboard/drm/drm_update_session.cc',
+ '<(DEPTH)/starboard/shared/starboard/event_cancel.cc',
+ '<(DEPTH)/starboard/shared/starboard/event_schedule.cc',
+ '<(DEPTH)/starboard/shared/starboard/file_mode_string_to_flags.cc',
+ '<(DEPTH)/starboard/shared/starboard/file_storage/storage_close_record.cc',
+ '<(DEPTH)/starboard/shared/starboard/file_storage/storage_delete_record.cc',
+ '<(DEPTH)/starboard/shared/starboard/file_storage/storage_get_record_size.cc',
+ '<(DEPTH)/starboard/shared/starboard/file_storage/storage_open_record.cc',
+ '<(DEPTH)/starboard/shared/starboard/file_storage/storage_read_record.cc',
+ '<(DEPTH)/starboard/shared/starboard/log_message.cc',
+ '<(DEPTH)/starboard/shared/starboard/log_raw_dump_stack.cc',
+ '<(DEPTH)/starboard/shared/starboard/log_raw_format.cc',
+ '<(DEPTH)/starboard/shared/starboard/media/codec_util.cc',
+ '<(DEPTH)/starboard/shared/starboard/media/codec_util.h',
+ '<(DEPTH)/starboard/shared/starboard/media/media_can_play_mime_and_key_system.cc',
+ '<(DEPTH)/starboard/shared/starboard/media/media_get_audio_buffer_budget.cc',
+ '<(DEPTH)/starboard/shared/starboard/media/media_get_buffer_alignment.cc',
+ '<(DEPTH)/starboard/shared/starboard/media/media_get_buffer_allocation_unit.cc',
+ '<(DEPTH)/starboard/shared/starboard/media/media_get_buffer_garbage_collection_duration_threshold.cc',
+ '<(DEPTH)/starboard/shared/starboard/media/media_get_buffer_padding.cc',
+ '<(DEPTH)/starboard/shared/starboard/media/media_get_buffer_storage_type.cc',
+ '<(DEPTH)/starboard/shared/starboard/media/media_get_progressive_buffer_budget.cc',
+ '<(DEPTH)/starboard/shared/starboard/media/media_get_video_buffer_budget.cc',
+ '<(DEPTH)/starboard/shared/starboard/media/media_is_buffer_pool_allocate_on_demand.cc',
+ '<(DEPTH)/starboard/shared/starboard/media/media_is_buffer_using_memory_pool.cc',
+ '<(DEPTH)/starboard/shared/starboard/media/media_util.cc',
+ '<(DEPTH)/starboard/shared/starboard/media/media_util.h',
+ '<(DEPTH)/starboard/shared/starboard/media/mime_type.cc',
+ '<(DEPTH)/starboard/shared/starboard/media/mime_type.h',
+ '<(DEPTH)/starboard/shared/starboard/microphone/microphone_close.cc',
+ '<(DEPTH)/starboard/shared/starboard/microphone/microphone_create.cc',
+ '<(DEPTH)/starboard/shared/starboard/microphone/microphone_destroy.cc',
+ '<(DEPTH)/starboard/shared/starboard/microphone/microphone_get_available.cc',
+ '<(DEPTH)/starboard/shared/starboard/microphone/microphone_internal.h',
+ '<(DEPTH)/starboard/shared/starboard/microphone/microphone_is_sample_rate_supported.cc',
+ '<(DEPTH)/starboard/shared/starboard/microphone/microphone_open.cc',
+ '<(DEPTH)/starboard/shared/starboard/microphone/microphone_read.cc',
+ '<(DEPTH)/starboard/shared/starboard/new.cc',
+ '<(DEPTH)/starboard/shared/starboard/player/decoded_audio_internal.cc',
+ '<(DEPTH)/starboard/shared/starboard/player/decoded_audio_internal.h',
+ '<(DEPTH)/starboard/shared/starboard/player/input_buffer_internal.cc',
+ '<(DEPTH)/starboard/shared/starboard/player/input_buffer_internal.h',
+ '<(DEPTH)/starboard/shared/starboard/player/job_queue.cc',
+ '<(DEPTH)/starboard/shared/starboard/player/job_queue.h',
+ '<(DEPTH)/starboard/shared/starboard/player/player_get_current_frame.cc',
+ '<(DEPTH)/starboard/shared/starboard/player/player_get_info.cc',
+ '<(DEPTH)/starboard/shared/starboard/player/player_get_info2.cc',
+ '<(DEPTH)/starboard/shared/starboard/player/player_get_maximum_number_of_samples_per_write.cc',
+ '<(DEPTH)/starboard/shared/starboard/player/player_internal.cc',
+ '<(DEPTH)/starboard/shared/starboard/player/player_internal.h',
+ '<(DEPTH)/starboard/shared/starboard/player/player_output_mode_supported.cc',
+ '<(DEPTH)/starboard/shared/starboard/player/player_seek.cc',
+ '<(DEPTH)/starboard/shared/starboard/player/player_seek2.cc',
+ '<(DEPTH)/starboard/shared/starboard/player/player_set_volume.cc',
+ '<(DEPTH)/starboard/shared/starboard/player/player_worker.cc',
+ '<(DEPTH)/starboard/shared/starboard/player/player_worker.h',
+ '<(DEPTH)/starboard/shared/starboard/player/player_write_end_of_stream.cc',
+ '<(DEPTH)/starboard/shared/starboard/player/player_write_sample.cc',
+ '<(DEPTH)/starboard/shared/starboard/player/player_write_sample2.cc',
+ '<(DEPTH)/starboard/shared/starboard/queue_application.cc',
+ '<(DEPTH)/starboard/shared/starboard/queue_application.h',
+ '<(DEPTH)/starboard/shared/starboard/speech_recognizer/speech_recognizer_cancel.cc',
+ '<(DEPTH)/starboard/shared/starboard/speech_recognizer/speech_recognizer_create.cc',
+ '<(DEPTH)/starboard/shared/starboard/speech_recognizer/speech_recognizer_destroy.cc',
+ '<(DEPTH)/starboard/shared/starboard/speech_recognizer/speech_recognizer_internal.h',
+ '<(DEPTH)/starboard/shared/starboard/speech_recognizer/speech_recognizer_start.cc',
+ '<(DEPTH)/starboard/shared/starboard/speech_recognizer/speech_recognizer_stop.cc',
+ '<(DEPTH)/starboard/shared/starboard/string_concat.cc',
+ '<(DEPTH)/starboard/shared/starboard/string_concat_wide.cc',
+ '<(DEPTH)/starboard/shared/starboard/string_copy.cc',
+ '<(DEPTH)/starboard/shared/starboard/string_copy_wide.cc',
+ '<(DEPTH)/starboard/shared/starboard/string_duplicate.cc',
+ '<(DEPTH)/starboard/shared/starboard/system_get_random_uint64.cc',
+ '<(DEPTH)/starboard/shared/starboard/system_supports_resume.cc',
+ '<(DEPTH)/starboard/shared/starboard/thread_checker.h',
+ '<(DEPTH)/starboard/shared/starboard/window_set_default_options.cc',
+ '<(DEPTH)/starboard/shared/stub/cryptography_create_transformer.cc',
+ '<(DEPTH)/starboard/shared/stub/cryptography_destroy_transformer.cc',
+ '<(DEPTH)/starboard/shared/stub/cryptography_get_tag.cc',
+ '<(DEPTH)/starboard/shared/stub/cryptography_set_authenticated_data.cc',
+ '<(DEPTH)/starboard/shared/stub/cryptography_set_initialization_vector.cc',
+ '<(DEPTH)/starboard/shared/stub/cryptography_transform.cc',
+ '<(DEPTH)/starboard/shared/stub/drm_is_server_certificate_updatable.cc',
+ '<(DEPTH)/starboard/shared/stub/drm_update_server_certificate.cc',
+ '<(DEPTH)/starboard/shared/stub/image_decode.cc',
+ '<(DEPTH)/starboard/shared/stub/image_is_decode_supported.cc',
+ '<(DEPTH)/starboard/shared/stub/system_get_total_gpu_memory.cc',
+ '<(DEPTH)/starboard/shared/stub/system_get_used_gpu_memory.cc',
+ '<(DEPTH)/starboard/shared/stub/system_hide_splash_screen.cc',
+ '<(DEPTH)/starboard/shared/stub/system_request_pause.cc',
+ '<(DEPTH)/starboard/shared/stub/system_request_unpause.cc',
+ '<(DEPTH)/starboard/shared/stub/thread_context_get_pointer.cc',
+ '<(DEPTH)/starboard/shared/stub/thread_sampler_create.cc',
+ '<(DEPTH)/starboard/shared/stub/thread_sampler_destroy.cc',
+ '<(DEPTH)/starboard/shared/stub/thread_sampler_freeze.cc',
+ '<(DEPTH)/starboard/shared/stub/thread_sampler_is_supported.cc',
+ '<(DEPTH)/starboard/shared/stub/thread_sampler_thaw.cc',
+ '<(DEPTH)/starboard/shared/stub/window_get_diagonal_size_in_inches.cc',
+ ],
+ 'conditions': [
+ ['has_input_events_filter=="True"', {
+ 'sources': [
+ 'input_events_filter.cc',
+ 'input_events_filter.h',
+ ],
+ 'defines': [
+ 'STARBOARD_INPUT_EVENTS_FILTER',
+ ],
+ }],
+ ],
+ 'defines': [
+ # This must be defined when building Starboard, and must not when
+ # building Starboard client code.
+ 'STARBOARD_IMPLEMENTATION',
+ ],
+ 'dependencies': [
+ '<(DEPTH)/starboard/common/common.gyp:common',
+ '<(DEPTH)/third_party/libevent/libevent.gyp:libevent',
+ 'starboard_base_symbolize',
+ ],
+ },
+ ],
+}
diff --git a/src/starboard/android/shared/starboard_platform_tests.gypi b/src/starboard/android/shared/starboard_platform_tests.gypi
new file mode 100644
index 0000000..5b40e63
--- /dev/null
+++ b/src/starboard/android/shared/starboard_platform_tests.gypi
@@ -0,0 +1,49 @@
+# Copyright 2016 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+{
+ 'targets': [
+ {
+ 'target_name': 'starboard_platform_tests',
+ 'type': '<(gtest_target_type)',
+ 'includes': [
+ '<(DEPTH)/starboard/shared/starboard/media/media_tests.gypi',
+ ],
+ 'sources': [
+ '<(DEPTH)/starboard/common/test_main.cc',
+ '<@(media_tests_sources)',
+ 'jni_env_ext_test.cc',
+ ],
+ 'defines': [
+ # This allows the tests to include internal only header files.
+ 'STARBOARD_IMPLEMENTATION',
+ ],
+ 'dependencies': [
+ '<(DEPTH)/starboard/starboard.gyp:starboard',
+ '<(DEPTH)/testing/gmock.gyp:gmock',
+ '<(DEPTH)/testing/gtest.gyp:gtest',
+ ],
+ },
+ {
+ 'target_name': 'starboard_platform_tests_deploy',
+ 'type': 'none',
+ 'dependencies': [
+ '<(DEPTH)/<(starboard_path)/starboard_platform_tests.gyp:starboard_platform_tests',
+ ],
+ 'variables': {
+ 'executable_name': 'starboard_platform_tests',
+ },
+ 'includes': [ '<(DEPTH)/starboard/build/deploy.gypi' ],
+ },
+ ],
+}
diff --git a/src/starboard/android/shared/system_get_connection_type.cc b/src/starboard/android/shared/system_get_connection_type.cc
new file mode 100644
index 0000000..228726d
--- /dev/null
+++ b/src/starboard/android/shared/system_get_connection_type.cc
@@ -0,0 +1,32 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/system.h"
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/log.h"
+
+using starboard::android::shared::JniEnvExt;
+
+SbSystemConnectionType SbSystemGetConnectionType() {
+ JniEnvExt* env = JniEnvExt::Get();
+ jboolean isWireless =
+ env->CallStarboardBooleanMethodOrAbort("isCurrentNetworkWireless", "()Z");
+
+ if (isWireless) {
+ return kSbSystemConnectionTypeWireless;
+ } else {
+ return kSbSystemConnectionTypeWired;
+ }
+}
diff --git a/src/starboard/android/shared/system_get_device_type.cc b/src/starboard/android/shared/system_get_device_type.cc
new file mode 100644
index 0000000..97d6a40
--- /dev/null
+++ b/src/starboard/android/shared/system_get_device_type.cc
@@ -0,0 +1,19 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/system.h"
+
+SbSystemDeviceType SbSystemGetDeviceType() {
+ return kSbSystemDeviceTypeAndroidTV;
+}
diff --git a/src/starboard/android/shared/system_get_locale_id.cc b/src/starboard/android/shared/system_get_locale_id.cc
new file mode 100644
index 0000000..2b11e0e
--- /dev/null
+++ b/src/starboard/android/shared/system_get_locale_id.cc
@@ -0,0 +1,49 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/system.h"
+
+#include <string>
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/once.h"
+#include "starboard/string.h"
+
+using starboard::android::shared::JniEnvExt;
+using starboard::android::shared::ScopedLocalJavaRef;
+
+namespace {
+
+// A singleton class to hold a locale string
+class LocaleInfo {
+ public:
+ // The Starboard locale id
+ std::string locale_id;
+
+ LocaleInfo() {
+ JniEnvExt* env = JniEnvExt::Get();
+
+ ScopedLocalJavaRef<jstring> result(env->CallStarboardObjectMethodOrAbort(
+ "systemGetLocaleId", "()Ljava/lang/String;"));
+ locale_id = env->GetStringStandardUTFOrAbort(result.Get());
+ }
+};
+
+SB_ONCE_INITIALIZE_FUNCTION(LocaleInfo, GetLocale);
+} // namespace
+
+const char* SbSystemGetLocaleId() {
+ return GetLocale()->locale_id.c_str();
+}
diff --git a/src/starboard/android/shared/system_get_path.cc b/src/starboard/android/shared/system_get_path.cc
new file mode 100644
index 0000000..3a2c323
--- /dev/null
+++ b/src/starboard/android/shared/system_get_path.cc
@@ -0,0 +1,109 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/system.h"
+
+#include <linux/limits.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include <cstring>
+
+#include "starboard/android/shared/file_internal.h"
+#include "starboard/directory.h"
+#include "starboard/log.h"
+#include "starboard/string.h"
+
+using ::starboard::android::shared::g_app_assets_dir;
+using ::starboard::android::shared::g_app_cache_dir;
+using ::starboard::android::shared::g_app_lib_dir;
+
+bool SbSystemGetPath(SbSystemPathId path_id, char* out_path, int path_size) {
+ if (!out_path || !path_size) {
+ return false;
+ }
+
+ const int kPathSize = PATH_MAX;
+ char path[kPathSize];
+ path[0] = '\0';
+
+ switch (path_id) {
+ case kSbSystemPathContentDirectory: {
+ if (SbStringConcat(path, g_app_assets_dir, kPathSize) >= kPathSize) {
+ return false;
+ }
+ break;
+ }
+
+ case kSbSystemPathCacheDirectory: {
+ if (!SbSystemGetPath(kSbSystemPathTempDirectory, path, kPathSize)) {
+ return false;
+ }
+ if (SbStringConcat(path, "/cache", kPathSize) >= kPathSize) {
+ return false;
+ }
+
+ SbDirectoryCreate(path);
+ break;
+ }
+
+ case kSbSystemPathDebugOutputDirectory: {
+ if (!SbSystemGetPath(kSbSystemPathTempDirectory, path, kPathSize)) {
+ return false;
+ }
+ if (SbStringConcat(path, "/log", kPathSize) >= kPathSize) {
+ return false;
+ }
+
+ SbDirectoryCreate(path);
+ break;
+ }
+
+ case kSbSystemPathTempDirectory: {
+ if (SbStringCopy(path, g_app_cache_dir, kPathSize) >= kPathSize) {
+ return false;
+ }
+
+ SbDirectoryCreate(path);
+ break;
+ }
+
+ case kSbSystemPathTestOutputDirectory: {
+ return SbSystemGetPath(kSbSystemPathDebugOutputDirectory, out_path,
+ path_size);
+ }
+
+ // We return the library directory as the "executable" since:
+ // a) Unlike the .so itself, it has a valid timestamp of the app install.
+ // b) Its parent directory is still a directory within our app package.
+ case kSbSystemPathExecutableFile: {
+ if (SbStringCopy(path, g_app_lib_dir, kPathSize) >= kPathSize) {
+ return false;
+ }
+ break;
+ }
+
+ default:
+ SB_NOTIMPLEMENTED();
+ return false;
+ }
+
+ int length = strlen(path);
+ if (length < 1 || length > path_size) {
+ return false;
+ }
+
+ SbStringCopy(out_path, path, path_size);
+ return true;
+}
diff --git a/src/starboard/android/shared/system_get_property.cc b/src/starboard/android/shared/system_get_property.cc
new file mode 100644
index 0000000..cbd49e1
--- /dev/null
+++ b/src/starboard/android/shared/system_get_property.cc
@@ -0,0 +1,137 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/system.h"
+
+#include "sys/system_properties.h"
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/log.h"
+#include "starboard/string.h"
+
+// We can't #include "base/stringize_macros.h" in Starboard
+#define STRINGIZE_NO_EXPANSION(x) #x
+#define STRINGIZE(x) STRINGIZE_NO_EXPANSION(x)
+
+using starboard::android::shared::JniEnvExt;
+using starboard::android::shared::ScopedLocalJavaRef;
+
+namespace {
+
+const char kFriendlyName[] = "Android";
+const char kUnknownValue[] = "unknown";
+// This is a format string template and the %s is meant to be replaced by
+// the Android release version number (e.g. "7.0" for Nougat).
+const char kPlatformNameFormat[] =
+ "Linux " STRINGIZE(ANDROID_ABI) "; Android %s";
+
+bool CopyStringAndTestIfSuccess(char* out_value,
+ int value_length,
+ const char* from_value) {
+ if (SbStringGetLength(from_value) + 1 > value_length)
+ return false;
+ SbStringCopy(out_value, from_value, value_length);
+ return true;
+}
+
+bool GetAndroidSystemProperty(const char* system_property_name,
+ char* out_value,
+ int value_length,
+ const char* default_value) {
+ if (value_length < PROP_VALUE_MAX) {
+ return false;
+ }
+ // Note that __system_property_get returns empty string on no value
+ __system_property_get(system_property_name, out_value);
+
+ if (SbStringGetLength(out_value) == 0) {
+ SbStringCopy(out_value, default_value, value_length);
+ }
+ return true;
+}
+
+// Populate the kPlatformNameFormat with the version number and if
+// |value_length| is large enough to store the result, copy the result into
+// |out_value|.
+bool CopyAndroidPlatformName(char* out_value, int value_length) {
+ // Get the Android version number (e.g. "7.0" for Nougat).
+ const int kStringBufferSize = 256;
+ char version_string_buffer[kStringBufferSize];
+ if (!GetAndroidSystemProperty("ro.build.version.release",
+ version_string_buffer, kStringBufferSize,
+ kUnknownValue)) {
+ return false;
+ }
+
+ char result_string[kStringBufferSize];
+ SbStringFormatF(result_string, kStringBufferSize, kPlatformNameFormat,
+ version_string_buffer);
+
+ return CopyStringAndTestIfSuccess(out_value, value_length, result_string);
+}
+
+} // namespace
+
+bool SbSystemGetProperty(SbSystemPropertyId property_id,
+ char* out_value,
+ int value_length) {
+ if (!out_value || !value_length) {
+ return false;
+ }
+
+ switch (property_id) {
+ case kSbSystemPropertyBrandName:
+ return GetAndroidSystemProperty("ro.product.manufacturer", out_value,
+ value_length, kUnknownValue);
+ case kSbSystemPropertyModelName:
+ return GetAndroidSystemProperty("ro.product.model", out_value,
+ value_length, kUnknownValue);
+ case kSbSystemPropertyFirmwareVersion:
+ return GetAndroidSystemProperty("ro.build.id", out_value, value_length,
+ kUnknownValue);
+ case kSbSystemPropertyChipsetModelNumber:
+ return GetAndroidSystemProperty("ro.board.platform", out_value,
+ value_length, kUnknownValue);
+ case kSbSystemPropertyModelYear:
+ case kSbSystemPropertyNetworkOperatorName:
+ return false;
+
+ case kSbSystemPropertyFriendlyName:
+ return CopyStringAndTestIfSuccess(out_value, value_length, kFriendlyName);
+
+ case kSbSystemPropertyPlatformName:
+ return CopyAndroidPlatformName(out_value, value_length);
+
+ case kSbSystemPropertySpeechApiKey:
+ return false;
+ case kSbSystemPropertyUserAgentAuxField: {
+ JniEnvExt* env = JniEnvExt::Get();
+ ScopedLocalJavaRef<jstring> aux_string(
+ env->CallStarboardObjectMethodOrAbort(
+ "getUserAgentAuxField", "()Ljava/lang/String;"));
+
+ std::string utf_str = env->GetStringStandardUTFOrAbort(aux_string.Get());
+ bool success =
+ CopyStringAndTestIfSuccess(out_value, value_length, utf_str.c_str());
+ return success;
+ }
+ default:
+ SB_DLOG(WARNING) << __FUNCTION__
+ << ": Unrecognized property: " << property_id;
+ break;
+ }
+
+ return false;
+}
diff --git a/src/starboard/android/shared/system_get_stack.cc b/src/starboard/android/shared/system_get_stack.cc
new file mode 100644
index 0000000..05434b4
--- /dev/null
+++ b/src/starboard/android/shared/system_get_stack.cc
@@ -0,0 +1,56 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <unwind.h>
+
+#include "starboard/system.h"
+#include "starboard/log.h"
+
+namespace {
+class CallbackContext {
+ public:
+ void** out_stack;
+ int stack_size;
+ int count;
+ CallbackContext(void** out_stack, int stack_size)
+ : out_stack(out_stack), stack_size(stack_size), count(0) {}
+};
+
+_Unwind_Reason_Code UnwindCallback(struct _Unwind_Context* uwc,
+ void* user_context) {
+ CallbackContext* callback_context =
+ static_cast<CallbackContext*>(user_context);
+ _Unwind_Ptr ip = _Unwind_GetIP(uwc);
+
+ if (ip == 0) {
+ return _URC_END_OF_STACK;
+ }
+
+ if (callback_context->count == callback_context->stack_size) {
+ return _URC_END_OF_STACK;
+ }
+
+ callback_context->out_stack[callback_context->count] =
+ reinterpret_cast<void*>(ip);
+ callback_context->count++;
+ return _URC_NO_REASON;
+}
+} // namespace
+
+int SbSystemGetStack(void** out_stack, int stack_size) {
+ CallbackContext callback_context(out_stack, stack_size);
+
+ _Unwind_Backtrace(UnwindCallback, &callback_context);
+ return callback_context.count;
+}
diff --git a/src/starboard/android/shared/system_has_capability.cc b/src/starboard/android/shared/system_has_capability.cc
new file mode 100644
index 0000000..4d77b0d
--- /dev/null
+++ b/src/starboard/android/shared/system_has_capability.cc
@@ -0,0 +1,31 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/system.h"
+
+#include "starboard/log.h"
+
+bool SbSystemHasCapability(SbSystemCapabilityId capability_id) {
+ switch (capability_id) {
+ case kSbSystemCapabilityReversedEnterAndBack:
+ return false;
+ case kSbSystemCapabilityCanQueryGPUMemoryStats:
+ return false;
+ case kSbSystemCapabilitySetsInputTimestamp:
+ return false;
+ }
+
+ SB_DLOG(WARNING) << "Unrecognized capability: " << capability_id;
+ return false;
+}
diff --git a/src/starboard/android/shared/system_platform_error.cc b/src/starboard/android/shared/system_platform_error.cc
new file mode 100644
index 0000000..1f66983
--- /dev/null
+++ b/src/starboard/android/shared/system_platform_error.cc
@@ -0,0 +1,92 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/system.h"
+
+#include <android/native_activity.h>
+#include <jni.h>
+
+#include "starboard/android/shared/application_android.h"
+#include "starboard/android/shared/jni_env_ext.h"
+
+using starboard::android::shared::ApplicationAndroid;
+using starboard::android::shared::JniEnvExt;
+
+struct SbSystemPlatformErrorPrivate {
+ SbSystemPlatformErrorPrivate(SbSystemPlatformErrorType type,
+ SbSystemPlatformErrorCallback callback,
+ void* user_data)
+ : error_obj(NULL), type(type), callback(callback), user_data(user_data) {}
+
+ jobject error_obj; // global ref to dev.cobalt.PlatformError instance
+ SbSystemPlatformErrorType type;
+ SbSystemPlatformErrorCallback callback;
+ void* user_data;
+};
+
+namespace {
+
+enum {
+ // This must be kept in sync with Java dev.cobalt.PlatformError.ErrorType
+ kJniErrorTypeConnectionError = 0,
+};
+
+} // namespace
+
+SbSystemPlatformError SbSystemRaisePlatformError(
+ SbSystemPlatformErrorType type,
+ SbSystemPlatformErrorCallback callback,
+ void* user_data) {
+ jint jni_error_type;
+ switch (type) {
+ case kSbSystemPlatformErrorTypeConnectionError:
+ jni_error_type = kJniErrorTypeConnectionError;
+ break;
+ default:
+ SB_NOTREACHED();
+ return kSbSystemPlatformErrorInvalid;
+ }
+
+ SbSystemPlatformError error_handle =
+ new SbSystemPlatformErrorPrivate(type, callback, user_data);
+
+ JniEnvExt* env = JniEnvExt::Get();
+ jobject error_obj = env->CallStarboardObjectMethodOrAbort(
+ "raisePlatformError", "(IJ)Ldev/cobalt/coat/PlatformError;",
+ jni_error_type, reinterpret_cast<jlong>(error_handle));
+ error_handle->error_obj = env->NewGlobalRef(error_obj);
+
+ return error_handle;
+}
+
+void SbSystemClearPlatformError(SbSystemPlatformError handle) {
+ JniEnvExt* env = JniEnvExt::Get();
+ env->CallObjectMethodOrAbort(handle->error_obj, "clear", "()V");
+}
+
+extern "C" SB_EXPORT_PLATFORM
+void Java_dev_cobalt_coat_PlatformError_nativeOnCleared(
+ JNIEnv* env, jobject unused_this, jint jni_response, jlong jni_data) {
+ SB_UNREFERENCED_PARAMETER(unused_this);
+ SbSystemPlatformError error_handle =
+ reinterpret_cast<SbSystemPlatformError>(jni_data);
+ env->DeleteGlobalRef(error_handle->error_obj);
+ if (error_handle->callback) {
+ SbSystemPlatformErrorResponse error_response =
+ jni_response < 0 ? kSbSystemPlatformErrorResponseNegative :
+ jni_response > 0 ? kSbSystemPlatformErrorResponsePositive :
+ kSbSystemPlatformErrorResponseCancel;
+ error_handle->callback(error_response, error_handle->user_data);
+ }
+}
diff --git a/src/starboard/android/shared/system_request_stop.cc b/src/starboard/android/shared/system_request_stop.cc
new file mode 100644
index 0000000..85aab60
--- /dev/null
+++ b/src/starboard/android/shared/system_request_stop.cc
@@ -0,0 +1,24 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/system.h"
+
+#include "starboard/android/shared/jni_env_ext.h"
+
+using starboard::android::shared::JniEnvExt;
+
+void SbSystemRequestStop(int error_level) {
+ JniEnvExt* env = JniEnvExt::Get();
+ env->CallStarboardVoidMethodOrAbort("requestStop", "(I)V", error_level);
+}
diff --git a/src/starboard/android/shared/system_request_suspend.cc b/src/starboard/android/shared/system_request_suspend.cc
new file mode 100644
index 0000000..c8f53c4
--- /dev/null
+++ b/src/starboard/android/shared/system_request_suspend.cc
@@ -0,0 +1,24 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/system.h"
+
+#include "starboard/android/shared/jni_env_ext.h"
+
+using starboard::android::shared::JniEnvExt;
+
+void SbSystemRequestSuspend() {
+ JniEnvExt* env = JniEnvExt::Get();
+ env->CallStarboardVoidMethodOrAbort("requestSuspend", "()V");
+}
diff --git a/src/starboard/android/shared/thread_create.cc b/src/starboard/android/shared/thread_create.cc
new file mode 100644
index 0000000..8aee6dd
--- /dev/null
+++ b/src/starboard/android/shared/thread_create.cc
@@ -0,0 +1,149 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/thread.h"
+
+#include <pthread.h>
+#include <sched.h>
+#include <sys/resource.h>
+#include <unistd.h>
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/log.h"
+#include "starboard/shared/pthread/is_success.h"
+#include "starboard/shared/pthread/thread_create_priority.h"
+#include "starboard/string.h"
+
+namespace starboard {
+namespace shared {
+namespace pthread {
+
+#if !SB_HAS(THREAD_PRIORITY_SUPPORT)
+// Default implementation without thread priority support
+void ThreadSetPriority(SbThreadPriority /* priority */) {}
+#endif
+
+void PreThreadRun() {}
+
+void PostThreadRun() {
+ // TODO: Currently OnThreadFinish() is specific to the android-forked version
+ // of this file, thread_create.cc. This functionality could be implemented
+ // though similar to ThreadSetPriority(), such that we allow platforms to
+ // override the OnThreadStart()/OnThreadFinish() logic, if they want, and then
+ // we don't have to fork this file. The only reason that is not done
+ // currently is because of bad timing with respect to Starboard interface
+ // changes.
+ android::shared::JniEnvExt::OnThreadShutdown();
+}
+
+} // namespace pthread
+} // namespace shared
+} // namespace starboard
+
+namespace {
+
+struct ThreadParams {
+ SbThreadAffinity affinity;
+ SbThreadEntryPoint entry_point;
+ char name[128];
+ void* context;
+ SbThreadPriority priority;
+};
+
+void* ThreadFunc(void* context) {
+ ThreadParams* thread_params = static_cast<ThreadParams*>(context);
+ SbThreadEntryPoint entry_point = thread_params->entry_point;
+ void* real_context = thread_params->context;
+ SbThreadAffinity affinity = thread_params->affinity;
+ if (thread_params->name[0] != '\0') {
+ SbThreadSetName(thread_params->name);
+ }
+
+ starboard::shared::pthread::ThreadSetPriority(thread_params->priority);
+
+ delete thread_params;
+
+ if (SbThreadIsValidAffinity(affinity)) {
+ cpu_set_t cpu_set;
+ CPU_ZERO(&cpu_set);
+ CPU_SET(affinity, &cpu_set);
+ sched_setaffinity(0, sizeof(cpu_set), &cpu_set);
+ }
+
+ starboard::shared::pthread::PreThreadRun();
+
+ void* return_value = entry_point(real_context);
+
+ starboard::shared::pthread::PostThreadRun();
+
+ return return_value;
+}
+
+} // namespace
+
+SbThread SbThreadCreate(int64_t stack_size,
+ SbThreadPriority priority,
+ SbThreadAffinity affinity,
+ bool joinable,
+ const char* name,
+ SbThreadEntryPoint entry_point,
+ void* context) {
+ if (stack_size < 0 || !entry_point) {
+ return kSbThreadInvalid;
+ }
+
+#if defined(ADDRESS_SANITIZER)
+ // Set a big thread stack size when in ADDRESS_SANITIZER mode.
+ // This eliminates buffer overflows for deeply nested callstacks.
+ if (stack_size == 0) {
+ stack_size = 4096 * 1024; // 4MB
+ }
+#endif
+
+ pthread_attr_t attributes;
+ int result = pthread_attr_init(&attributes);
+ if (!IsSuccess(result)) {
+ return kSbThreadInvalid;
+ }
+
+ pthread_attr_setdetachstate(
+ &attributes,
+ (joinable ? PTHREAD_CREATE_JOINABLE : PTHREAD_CREATE_DETACHED));
+ if (stack_size > 0) {
+ pthread_attr_setstacksize(&attributes, stack_size);
+ }
+
+ ThreadParams* params = new ThreadParams();
+ params->affinity = affinity;
+ params->entry_point = entry_point;
+ params->context = context;
+
+ if (name) {
+ SbStringCopy(params->name, name, SB_ARRAY_SIZE_INT(params->name));
+ } else {
+ params->name[0] = '\0';
+ }
+
+ params->priority = priority;
+
+ SbThread thread = kSbThreadInvalid;
+ result = pthread_create(&thread, &attributes, ThreadFunc, params);
+
+ pthread_attr_destroy(&attributes);
+ if (IsSuccess(result)) {
+ return thread;
+ }
+
+ return kSbThreadInvalid;
+}
diff --git a/src/starboard/android/shared/thread_create_priority.cc b/src/starboard/android/shared/thread_create_priority.cc
new file mode 100644
index 0000000..5247c58
--- /dev/null
+++ b/src/starboard/android/shared/thread_create_priority.cc
@@ -0,0 +1,68 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/shared/pthread/thread_create_priority.h"
+
+#include <sched.h>
+#include <sys/resource.h>
+
+#include "starboard/log.h"
+
+namespace starboard {
+namespace shared {
+namespace pthread {
+
+#if SB_HAS(THREAD_PRIORITY_SUPPORT)
+
+void SetNiceValue(int nice) {
+ int result = setpriority(PRIO_PROCESS, 0, nice);
+ if (result != 0) {
+ SB_NOTREACHED();
+ }
+}
+
+void ThreadSetPriority(SbThreadPriority priority) {
+ // Nice value settings are selected from looking at:
+ // https://android.googlesource.com/platform/frameworks/native/+/jb-dev/include/utils/ThreadDefs.h#35
+ switch (priority) {
+ case kSbThreadPriorityLowest:
+ SetNiceValue(19);
+ break;
+ case kSbThreadPriorityLow:
+ SetNiceValue(10);
+ break;
+ case kSbThreadNoPriority:
+ case kSbThreadPriorityNormal:
+ SetNiceValue(0);
+ break;
+ case kSbThreadPriorityHigh:
+ SetNiceValue(-8);
+ break;
+ case kSbThreadPriorityHighest:
+ SetNiceValue(-16);
+ break;
+ case kSbThreadPriorityRealTime:
+ SetNiceValue(-19);
+ break;
+ default:
+ SB_NOTREACHED();
+ break;
+ }
+}
+
+#endif // SB_HAS(THREAD_PRIORITY_SUPPORT)
+
+} // namespace pthread
+} // namespace shared
+} // namespace starboard
diff --git a/src/starboard/android/shared/thread_get_name.cc b/src/starboard/android/shared/thread_get_name.cc
new file mode 100644
index 0000000..3adbf9a
--- /dev/null
+++ b/src/starboard/android/shared/thread_get_name.cc
@@ -0,0 +1,20 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/thread.h"
+#include <sys/prctl.h>
+
+void SbThreadGetName(char* buffer, int buffer_size) {
+ prctl(PR_GET_NAME, buffer, 0L, 0L, 0L);
+}
diff --git a/src/starboard/android/shared/thread_types_public.h b/src/starboard/android/shared/thread_types_public.h
new file mode 100644
index 0000000..266f513
--- /dev/null
+++ b/src/starboard/android/shared/thread_types_public.h
@@ -0,0 +1,22 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Includes threading primitive types and initializers.
+
+#ifndef STARBOARD_ANDROID_SHARED_THREAD_TYPES_PUBLIC_H_
+#define STARBOARD_ANDROID_SHARED_THREAD_TYPES_PUBLIC_H_
+
+#include "starboard/shared/pthread/types_public.h"
+
+#endif // STARBOARD_ANDROID_SHARED_THREAD_TYPES_PUBLIC_H_
diff --git a/src/starboard/android/shared/time_zone_get_dst_name.cc b/src/starboard/android/shared/time_zone_get_dst_name.cc
new file mode 100644
index 0000000..fbdeb86
--- /dev/null
+++ b/src/starboard/android/shared/time_zone_get_dst_name.cc
@@ -0,0 +1,33 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/time_zone.h"
+
+#include <time.h>
+
+#if SB_API_VERSION < 6
+const char* SbTimeZoneGetDstName() {
+ // Note tzset() is called in ApplicationAndroid::Initialize()
+
+ // Android's bionic seems not to set tzname[1] when selecting GMT
+ // because timezone is not otherwise available.
+ // But glibc returns "GMT" in both tzname[0] and tzname[1] when
+ // GMT is selected.
+ if (tzname[1][0] == '\0') {
+ return tzname[0];
+ } else {
+ return tzname[1];
+ }
+}
+#endif // SB_API_VERSION < 6
diff --git a/src/starboard/android/shared/time_zone_get_name.cc b/src/starboard/android/shared/time_zone_get_name.cc
new file mode 100644
index 0000000..b2b6c41
--- /dev/null
+++ b/src/starboard/android/shared/time_zone_get_name.cc
@@ -0,0 +1,23 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/time_zone.h"
+
+#include <time.h>
+
+const char* SbTimeZoneGetName() {
+ // Note tzset() is called in ApplicationAndroid::Initialize()
+
+ return tzname[0];
+}
diff --git a/src/starboard/android/shared/trace_util.h b/src/starboard/android/shared/trace_util.h
new file mode 100644
index 0000000..96a8ac2
--- /dev/null
+++ b/src/starboard/android/shared/trace_util.h
@@ -0,0 +1,46 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_TRACE_UTIL_H_
+#define STARBOARD_ANDROID_SHARED_TRACE_UTIL_H_
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+// A simple scoped wrapper of |android.os.Trace|.
+struct ScopedTrace {
+ explicit ScopedTrace(const char* section_name) {
+ JniEnvExt* env = JniEnvExt::Get();
+ ScopedLocalJavaRef<jstring> j_section_name(
+ env->NewStringStandardUTFOrAbort(section_name));
+ env->CallStaticVoidMethodOrAbort("android/os/Trace", "beginSection",
+ "(Ljava/lang/String;)V",
+ j_section_name.Get());
+ }
+
+ ~ScopedTrace() {
+ JniEnvExt* env = JniEnvExt::Get();
+ env->CallStaticVoidMethodOrAbort("android/os/Trace", "endSection", "()V");
+ }
+};
+
+} // namespace shared
+} // namespace android
+} // namespace starboard
+
+#endif // STARBOARD_ANDROID_SHARED_TRACE_UTIL_H_
diff --git a/src/starboard/android/shared/video_decoder.cc b/src/starboard/android/shared/video_decoder.cc
new file mode 100644
index 0000000..a27d637
--- /dev/null
+++ b/src/starboard/android/shared/video_decoder.cc
@@ -0,0 +1,522 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/video_decoder.h"
+
+#include <jni.h>
+
+#include <cmath>
+#include <functional>
+
+#include "starboard/android/shared/application_android.h"
+#include "starboard/android/shared/decode_target_create.h"
+#include "starboard/android/shared/decode_target_internal.h"
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/android/shared/media_common.h"
+#include "starboard/android/shared/video_window.h"
+#include "starboard/android/shared/window_internal.h"
+#include "starboard/configuration.h"
+#include "starboard/decode_target.h"
+#include "starboard/drm.h"
+#include "starboard/memory.h"
+#include "starboard/shared/starboard/player/filter/video_frame_internal.h"
+#include "starboard/string.h"
+#include "starboard/thread.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+namespace {
+
+using ::starboard::shared::starboard::player::filter::VideoFrame;
+using std::placeholders::_1;
+using std::placeholders::_2;
+
+class VideoFrameImpl : public VideoFrame {
+ public:
+ VideoFrameImpl(const DequeueOutputResult& dequeue_output_result,
+ MediaCodecBridge* media_codec_bridge)
+ : VideoFrame(dequeue_output_result.flags & BUFFER_FLAG_END_OF_STREAM
+ ? kMediaTimeEndOfStream
+ : dequeue_output_result.presentation_time_microseconds),
+ dequeue_output_result_(dequeue_output_result),
+ media_codec_bridge_(media_codec_bridge),
+ released_(false) {
+ SB_DCHECK(media_codec_bridge_);
+ }
+
+ ~VideoFrameImpl() {
+ if (!released_) {
+ media_codec_bridge_->ReleaseOutputBuffer(dequeue_output_result_.index,
+ false);
+ if (is_end_of_stream()) {
+ media_codec_bridge_->Flush();
+ }
+ }
+ }
+
+ void Draw(int64_t release_time_in_nanoseconds) {
+ SB_DCHECK(!released_);
+ SB_DCHECK(!is_end_of_stream());
+ released_ = true;
+ media_codec_bridge_->ReleaseOutputBufferAtTimestamp(
+ dequeue_output_result_.index, release_time_in_nanoseconds);
+ }
+
+ private:
+ DequeueOutputResult dequeue_output_result_;
+ MediaCodecBridge* media_codec_bridge_;
+ volatile bool released_;
+};
+
+const SbTime kInitialPrerollTimeout = 250 * kSbTimeMillisecond;
+
+const int kInitialPrerollFrameCount = 8;
+const int kNonInitialPrerollFrameCount = 1;
+
+const int kMaxPendingWorkSize = 128;
+
+// Convenience HDR mastering metadata.
+const SbMediaMasteringMetadata kEmptyMasteringMetadata = {};
+
+// Determine if two |SbMediaMasteringMetadata|s are equal.
+bool Equal(const SbMediaMasteringMetadata& lhs,
+ const SbMediaMasteringMetadata& rhs) {
+ return SbMemoryCompare(&lhs, &rhs, sizeof(SbMediaMasteringMetadata)) == 0;
+}
+
+// Determine if two |SbMediaColorMetadata|s are equal.
+bool Equal(const SbMediaColorMetadata& lhs, const SbMediaColorMetadata& rhs) {
+ return SbMemoryCompare(&lhs, &rhs, sizeof(SbMediaMasteringMetadata)) == 0;
+}
+
+// TODO: For whatever reason, Cobalt will always pass us this us for
+// color metadata, regardless of whether HDR is on or not. Find out if this
+// is intentional or not. It would make more sense if it were NULL.
+// Determine if |color_metadata| is "empty", or "null".
+bool IsIdentity(const SbMediaColorMetadata& color_metadata) {
+ return color_metadata.primaries == kSbMediaPrimaryIdBt709 &&
+ color_metadata.transfer == kSbMediaTransferIdBt709 &&
+ color_metadata.matrix == kSbMediaMatrixIdBt709 &&
+ color_metadata.range == kSbMediaRangeIdLimited &&
+ Equal(color_metadata.mastering_metadata, kEmptyMasteringMetadata);
+}
+
+} // namespace
+
+class VideoDecoder::Sink : public VideoDecoder::VideoRendererSink {
+ public:
+ bool Render() {
+ SB_DCHECK(render_cb_);
+
+ rendered_ = false;
+ render_cb_(std::bind(&Sink::DrawFrame, this, _1, _2));
+
+ return rendered_;
+ }
+
+ private:
+ void SetRenderCB(RenderCB render_cb) override {
+ SB_DCHECK(!render_cb_);
+ SB_DCHECK(render_cb);
+
+ render_cb_ = render_cb;
+ }
+
+ void SetBounds(int z_index, int x, int y, int width, int height) override {
+ SB_UNREFERENCED_PARAMETER(z_index);
+ SB_UNREFERENCED_PARAMETER(x);
+ SB_UNREFERENCED_PARAMETER(y);
+ SB_UNREFERENCED_PARAMETER(width);
+ SB_UNREFERENCED_PARAMETER(height);
+ }
+
+ DrawFrameStatus DrawFrame(const scoped_refptr<VideoFrame>& frame,
+ int64_t release_time_in_nanoseconds) {
+ rendered_ = true;
+ static_cast<VideoFrameImpl*>(frame.get())
+ ->Draw(release_time_in_nanoseconds);
+
+ return kReleased;
+ }
+
+ RenderCB render_cb_;
+ bool rendered_;
+};
+
+VideoDecoder::VideoDecoder(SbMediaVideoCodec video_codec,
+ SbDrmSystem drm_system,
+ SbPlayerOutputMode output_mode,
+ SbDecodeTargetGraphicsContextProvider*
+ decode_target_graphics_context_provider)
+ : video_codec_(video_codec),
+ drm_system_(static_cast<DrmSystem*>(drm_system)),
+ output_mode_(output_mode),
+ decode_target_graphics_context_provider_(
+ decode_target_graphics_context_provider),
+ decode_target_(kSbDecodeTargetInvalid),
+ frame_width_(0),
+ frame_height_(0),
+ first_buffer_received_(false) {
+ if (!InitializeCodec()) {
+ SB_LOG(ERROR) << "Failed to initialize video decoder.";
+ TeardownCodec();
+ }
+}
+
+VideoDecoder::~VideoDecoder() {
+ TeardownCodec();
+ ClearVideoWindow();
+}
+
+scoped_refptr<VideoDecoder::VideoRendererSink> VideoDecoder::GetSink() {
+ if (sink_ == NULL) {
+ sink_ = new Sink;
+ }
+ return sink_;
+}
+
+void VideoDecoder::Initialize(const DecoderStatusCB& decoder_status_cb,
+ const ErrorCB& error_cb) {
+ SB_DCHECK(media_decoder_);
+
+ SB_DCHECK(decoder_status_cb);
+ SB_DCHECK(!decoder_status_cb_);
+ SB_DCHECK(error_cb);
+ SB_DCHECK(!error_cb_);
+
+ decoder_status_cb_ = decoder_status_cb;
+ error_cb_ = error_cb;
+
+ media_decoder_->Initialize(error_cb_);
+}
+
+size_t VideoDecoder::GetPrerollFrameCount() const {
+ if (first_buffer_received_ && first_buffer_timestamp_ != 0) {
+ return kNonInitialPrerollFrameCount;
+ }
+ return kInitialPrerollFrameCount;
+}
+
+SbTime VideoDecoder::GetPrerollTimeout() const {
+ if (first_buffer_received_ && first_buffer_timestamp_ != 0) {
+ return kSbTimeMax;
+ }
+ return kInitialPrerollTimeout;
+}
+
+void VideoDecoder::WriteInputBuffer(
+ const scoped_refptr<InputBuffer>& input_buffer) {
+ SB_DCHECK(input_buffer);
+ SB_DCHECK(decoder_status_cb_);
+
+ if (!first_buffer_received_) {
+ first_buffer_received_ = true;
+ first_buffer_timestamp_ = input_buffer->timestamp();
+
+ // If color metadata is present and is not an identity mapping, then
+ // teardown the codec so it can be reinitalized with the new metadata.
+ auto* color_metadata = input_buffer->video_sample_info()->color_metadata;
+ if (color_metadata && !IsIdentity(*color_metadata)) {
+ SB_DCHECK(!color_metadata_) << "Unexpected residual color metadata.";
+ SB_LOG(INFO) << "Reinitializing codec with HDR color metadata.";
+ TeardownCodec();
+ color_metadata_ = *color_metadata;
+ }
+
+ // Re-initialize the codec now if it was torn down either in |Reset| or
+ // because we need to change the color metadata.
+ if (media_decoder_ == NULL) {
+ if (!InitializeCodec()) {
+ // TODO: Communicate this failure to our clients somehow.
+ SB_LOG(ERROR) << "Failed to reinitialize codec.";
+ }
+ }
+ }
+
+ media_decoder_->WriteInputBuffer(input_buffer);
+ if (number_of_frames_being_decoded_.increment() < kMaxPendingWorkSize) {
+ decoder_status_cb_(kNeedMoreInput, NULL);
+ }
+}
+
+void VideoDecoder::WriteEndOfStream() {
+ SB_DCHECK(decoder_status_cb_);
+
+ if (!first_buffer_received_) {
+ first_buffer_received_ = true;
+ first_buffer_timestamp_ = 0;
+ }
+
+ media_decoder_->WriteEndOfStream();
+}
+
+void VideoDecoder::Reset() {
+ TeardownCodec();
+ number_of_frames_being_decoded_.store(0);
+ first_buffer_received_ = false;
+}
+
+bool VideoDecoder::InitializeCodec() {
+ // Setup the output surface object. If we are in punch-out mode, target
+ // the passed in Android video surface. If we are in decode-to-texture
+ // mode, create a surface from a new texture target and use that as the
+ // output surface.
+ jobject j_output_surface = NULL;
+ switch (output_mode_) {
+ case kSbPlayerOutputModePunchOut: {
+ j_output_surface = GetVideoSurface();
+ } break;
+ case kSbPlayerOutputModeDecodeToTexture: {
+ // A width and height of (0, 0) is provided here because Android doesn't
+ // actually allocate any memory into the texture at this time. That is
+ // done behind the scenes, the acquired texture is not actually backed
+ // by texture data until updateTexImage() is called on it.
+ SbDecodeTarget decode_target =
+ DecodeTargetCreate(decode_target_graphics_context_provider_,
+ kSbDecodeTargetFormat1PlaneRGBA, 0, 0);
+ if (!SbDecodeTargetIsValid(decode_target)) {
+ SB_LOG(ERROR) << "Could not acquire a decode target from provider.";
+ return false;
+ }
+ j_output_surface = decode_target->data->surface;
+
+ starboard::ScopedLock lock(decode_target_mutex_);
+ decode_target_ = decode_target;
+ } break;
+ case kSbPlayerOutputModeInvalid: {
+ SB_NOTREACHED();
+ } break;
+ }
+ if (!j_output_surface) {
+ SB_LOG(ERROR) << "Video surface does not exist.";
+ return false;
+ }
+
+ ANativeWindow* video_window = GetVideoWindow();
+ if (!video_window) {
+ SB_LOG(ERROR)
+ << "Can't initialize the codec since we don't have a video window.";
+ return false;
+ }
+ int width = ANativeWindow_getWidth(video_window);
+ int height = ANativeWindow_getHeight(video_window);
+
+ jobject j_media_crypto = drm_system_ ? drm_system_->GetMediaCrypto() : NULL;
+ SB_DCHECK(!drm_system_ || j_media_crypto);
+ media_decoder_.reset(new MediaDecoder(
+ this, video_codec_, width, height, j_output_surface, drm_system_,
+ color_metadata_ ? &*color_metadata_ : nullptr));
+ if (media_decoder_->is_valid()) {
+ if (error_cb_) {
+ media_decoder_->Initialize(error_cb_);
+ }
+ return true;
+ }
+ media_decoder_.reset();
+ return false;
+}
+
+void VideoDecoder::TeardownCodec() {
+ media_decoder_.reset();
+ color_metadata_ = starboard::nullopt;
+
+ starboard::ScopedLock lock(decode_target_mutex_);
+ if (SbDecodeTargetIsValid(decode_target_)) {
+ SbDecodeTargetReleaseInGlesContext(decode_target_graphics_context_provider_,
+ decode_target_);
+ decode_target_ = kSbDecodeTargetInvalid;
+ }
+}
+
+void VideoDecoder::ProcessOutputBuffer(
+ MediaCodecBridge* media_codec_bridge,
+ const DequeueOutputResult& dequeue_output_result) {
+ SB_DCHECK(decoder_status_cb_);
+ SB_DCHECK(dequeue_output_result.index >= 0);
+
+ number_of_frames_being_decoded_.decrement();
+ bool is_end_of_stream =
+ dequeue_output_result.flags & BUFFER_FLAG_END_OF_STREAM;
+ decoder_status_cb_(
+ is_end_of_stream ? kBufferFull : kNeedMoreInput,
+ new VideoFrameImpl(dequeue_output_result, media_codec_bridge));
+}
+
+void VideoDecoder::RefreshOutputFormat(MediaCodecBridge* media_codec_bridge) {
+ SB_DCHECK(media_codec_bridge);
+ SB_DLOG(INFO) << "Output format changed, trying to dequeue again.";
+ // Record the latest width/height of the decoded input.
+ SurfaceDimensions output_dimensions =
+ media_codec_bridge->GetOutputDimensions();
+ frame_width_ = output_dimensions.width;
+ frame_height_ = output_dimensions.height;
+}
+
+bool VideoDecoder::Tick(MediaCodecBridge* media_codec_bridge) {
+ return sink_->Render();
+}
+
+void VideoDecoder::OnFlushing() {
+ decoder_status_cb_(kReleaseAllFrames, NULL);
+}
+
+namespace {
+void updateTexImage(jobject surface_texture) {
+ JniEnvExt* env = JniEnvExt::Get();
+ env->CallVoidMethodOrAbort(surface_texture, "updateTexImage", "()V");
+}
+
+void getTransformMatrix(jobject surface_texture, float* matrix4x4) {
+ JniEnvExt* env = JniEnvExt::Get();
+
+ jfloatArray java_array = env->NewFloatArray(16);
+ SB_DCHECK(java_array);
+
+ env->CallVoidMethodOrAbort(surface_texture, "getTransformMatrix", "([F)V",
+ java_array);
+
+ jfloat* array_values = env->GetFloatArrayElements(java_array, 0);
+ SbMemoryCopy(matrix4x4, array_values, sizeof(float) * 16);
+
+ env->DeleteLocalRef(java_array);
+}
+
+// Rounds the float to the nearest integer, and also does a DCHECK to make sure
+// that the input float was already near an integer value.
+int RoundToNearInteger(float x) {
+ int rounded = static_cast<int>(x + 0.5f);
+ return rounded;
+}
+
+// Converts a 4x4 matrix representing the texture coordinate transform into
+// an equivalent rectangle representing the region within the texture where
+// the pixel data is valid. Note that the width and height of this region may
+// be negative to indicate that that axis should be flipped.
+void SetDecodeTargetContentRegionFromMatrix(
+ SbDecodeTargetInfoContentRegion* content_region,
+ int width,
+ int height,
+ const float* matrix4x4) {
+ // Ensure that this matrix contains no rotations or shears. In other words,
+ // make sure that we can convert it to a decode target content region without
+ // losing any information.
+ SB_DCHECK(matrix4x4[1] == 0.0f);
+ SB_DCHECK(matrix4x4[2] == 0.0f);
+ SB_DCHECK(matrix4x4[3] == 0.0f);
+
+ SB_DCHECK(matrix4x4[4] == 0.0f);
+ SB_DCHECK(matrix4x4[6] == 0.0f);
+ SB_DCHECK(matrix4x4[7] == 0.0f);
+
+ SB_DCHECK(matrix4x4[8] == 0.0f);
+ SB_DCHECK(matrix4x4[9] == 0.0f);
+ SB_DCHECK(matrix4x4[10] == 1.0f);
+ SB_DCHECK(matrix4x4[11] == 0.0f);
+
+ SB_DCHECK(matrix4x4[14] == 0.0f);
+ SB_DCHECK(matrix4x4[15] == 1.0f);
+
+ float origin_x = matrix4x4[12];
+ float origin_y = matrix4x4[13];
+
+ float extent_x = matrix4x4[0] + matrix4x4[12];
+ float extent_y = matrix4x4[5] + matrix4x4[13];
+
+ SB_DCHECK(origin_y >= 0.0f);
+ SB_DCHECK(origin_y <= 1.0f);
+ SB_DCHECK(origin_x >= 0.0f);
+ SB_DCHECK(origin_x <= 1.0f);
+ SB_DCHECK(extent_x >= 0.0f);
+ SB_DCHECK(extent_x <= 1.0f);
+ SB_DCHECK(extent_y >= 0.0f);
+ SB_DCHECK(extent_y <= 1.0f);
+
+ // Flip the y-axis to match ContentRegion's coordinate system.
+ origin_y = 1.0f - origin_y;
+ extent_y = 1.0f - extent_y;
+
+ content_region->left = RoundToNearInteger(origin_x * width);
+ content_region->right = RoundToNearInteger(extent_x * width);
+
+ // Note that in GL coordinates, the origin is the bottom and the extent
+ // is the top.
+ content_region->top = RoundToNearInteger(extent_y * height);
+ content_region->bottom = RoundToNearInteger(origin_y * height);
+}
+} // namespace
+
+// When in decode-to-texture mode, this returns the current decoded video frame.
+SbDecodeTarget VideoDecoder::GetCurrentDecodeTarget() {
+ SB_DCHECK(output_mode_ == kSbPlayerOutputModeDecodeToTexture);
+ // We must take a lock here since this function can be called from a separate
+ // thread.
+ starboard::ScopedLock lock(decode_target_mutex_);
+
+ if (SbDecodeTargetIsValid(decode_target_)) {
+ updateTexImage(decode_target_->data->surface_texture);
+
+ float matrix4x4[16];
+ getTransformMatrix(decode_target_->data->surface_texture, matrix4x4);
+ SetDecodeTargetContentRegionFromMatrix(
+ &decode_target_->data->info.planes[0].content_region, frame_width_,
+ frame_height_, matrix4x4);
+
+ // Take this opportunity to update the decode target's width and height.
+ decode_target_->data->info.planes[0].width = frame_width_;
+ decode_target_->data->info.planes[0].height = frame_height_;
+ decode_target_->data->info.width = frame_width_;
+ decode_target_->data->info.height = frame_height_;
+
+ SbDecodeTarget out_decode_target = new SbDecodeTargetPrivate;
+ out_decode_target->data = decode_target_->data;
+
+ return out_decode_target;
+ } else {
+ return kSbDecodeTargetInvalid;
+ }
+}
+
+} // namespace shared
+} // namespace android
+} // namespace starboard
+
+namespace starboard {
+namespace shared {
+namespace starboard {
+namespace player {
+namespace filter {
+
+// static
+bool VideoDecoder::OutputModeSupported(SbPlayerOutputMode output_mode,
+ SbMediaVideoCodec codec,
+ SbDrmSystem drm_system) {
+ if (output_mode == kSbPlayerOutputModePunchOut) {
+ return true;
+ }
+
+ if (output_mode == kSbPlayerOutputModeDecodeToTexture) {
+ return !SbDrmSystemIsValid(drm_system);
+ }
+
+ return false;
+}
+
+} // namespace filter
+} // namespace player
+} // namespace starboard
+} // namespace shared
+} // namespace starboard
diff --git a/src/starboard/android/shared/video_decoder.h b/src/starboard/android/shared/video_decoder.h
new file mode 100644
index 0000000..0a4bd13
--- /dev/null
+++ b/src/starboard/android/shared/video_decoder.h
@@ -0,0 +1,124 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_VIDEO_DECODER_H_
+#define STARBOARD_ANDROID_SHARED_VIDEO_DECODER_H_
+
+#include <deque>
+
+#include "starboard/android/shared/drm_system.h"
+#include "starboard/android/shared/media_codec_bridge.h"
+#include "starboard/android/shared/media_decoder.h"
+#include "starboard/atomic.h"
+#include "starboard/common/optional.h"
+#include "starboard/common/ref_counted.h"
+#include "starboard/decode_target.h"
+#include "starboard/media.h"
+#include "starboard/player.h"
+#include "starboard/shared/internal_only.h"
+#include "starboard/shared/starboard/player/filter/video_decoder_internal.h"
+#include "starboard/shared/starboard/player/filter/video_renderer_sink.h"
+#include "starboard/shared/starboard/player/input_buffer_internal.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+class VideoDecoder
+ : public ::starboard::shared::starboard::player::filter::VideoDecoder,
+ private MediaDecoder::Host {
+ public:
+ typedef ::starboard::shared::starboard::player::filter::VideoRendererSink
+ VideoRendererSink;
+
+ class Sink;
+
+ VideoDecoder(SbMediaVideoCodec video_codec,
+ SbDrmSystem drm_system,
+ SbPlayerOutputMode output_mode,
+ SbDecodeTargetGraphicsContextProvider*
+ decode_target_graphics_context_provider);
+ ~VideoDecoder() override;
+
+ scoped_refptr<VideoRendererSink> GetSink();
+
+ void Initialize(const DecoderStatusCB& decoder_status_cb,
+ const ErrorCB& error_cb) override;
+ size_t GetPrerollFrameCount() const override;
+ SbTime GetPrerollTimeout() const override;
+ size_t GetMaxNumberOfCachedFrames() const override { return 12; }
+
+ void WriteInputBuffer(const scoped_refptr<InputBuffer>& input_buffer)
+ override;
+ void WriteEndOfStream() override;
+ void Reset() override;
+ SbDecodeTarget GetCurrentDecodeTarget() override;
+
+ bool is_valid() const { return media_decoder_ != NULL; }
+
+ private:
+ // Attempt to initialize the codec. Returns whether initialization was
+ // successful.
+ bool InitializeCodec();
+ void TeardownCodec();
+
+ void ProcessOutputBuffer(MediaCodecBridge* media_codec_bridge,
+ const DequeueOutputResult& output) override;
+ void RefreshOutputFormat(MediaCodecBridge* media_codec_bridge) override;
+ bool Tick(MediaCodecBridge* media_codec_bridge) override;
+ void OnFlushing() override;
+
+ // These variables will be initialized inside ctor or Initialize() and will
+ // not be changed during the life time of this class.
+ const SbMediaVideoCodec video_codec_;
+ DecoderStatusCB decoder_status_cb_;
+ ErrorCB error_cb_;
+ DrmSystem* drm_system_;
+
+ SbPlayerOutputMode output_mode_;
+
+ SbDecodeTargetGraphicsContextProvider*
+ decode_target_graphics_context_provider_;
+
+ // If decode-to-texture is enabled, then we store the decode target texture
+ // inside of this |decode_target_| member.
+ SbDecodeTarget decode_target_;
+
+ // Since GetCurrentDecodeTarget() needs to be called from an arbitrary thread
+ // to obtain the current decode target (which ultimately ends up being a
+ // copy of |decode_target_|), we need to safe-guard access to |decode_target_|
+ // and we do so through this mutex.
+ starboard::Mutex decode_target_mutex_;
+
+ // The width and height of the latest decoded frame.
+ int32_t frame_width_;
+ int32_t frame_height_;
+
+ // The last enqueued |SbMediaColorMetadata|.
+ optional<SbMediaColorMetadata> color_metadata_;
+
+ scoped_ptr<MediaDecoder> media_decoder_;
+
+ atomic_int32_t number_of_frames_being_decoded_;
+ scoped_refptr<Sink> sink_;
+
+ bool first_buffer_received_;
+ volatile SbTime first_buffer_timestamp_;
+};
+
+} // namespace shared
+} // namespace android
+} // namespace starboard
+
+#endif // STARBOARD_ANDROID_SHARED_VIDEO_DECODER_H_
diff --git a/src/starboard/android/shared/video_frame_release_time_adjuster.cc b/src/starboard/android/shared/video_frame_release_time_adjuster.cc
new file mode 100644
index 0000000..30f4b07
--- /dev/null
+++ b/src/starboard/android/shared/video_frame_release_time_adjuster.cc
@@ -0,0 +1,170 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/video_frame_release_time_adjuster.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+VideoFrameReleaseTimeHelper::VideoFrameReleaseTimeHelper() {
+ this(DISPLAY_REFRESH_RATE_UNKNOWN);
+}
+
+VideoFrameReleaseTimeHelper::VideoFrameReleaseTimeHelper(Context context) {
+ this(getDefaultDisplayRefreshRate(context));
+}
+
+VideoFrameReleaseTimeHelper::VideoFrameReleaseTimeHelper(
+ double defaultDisplayRefreshRate) {
+ useDefaultDisplayVsync =
+ defaultDisplayRefreshRate != DISPLAY_REFRESH_RATE_UNKNOWN;
+ if (useDefaultDisplayVsync) {
+ vsyncSampler = VSyncSampler.getInstance();
+ vsyncDurationNs = (long)(NANOS_PER_SECOND / defaultDisplayRefreshRate);
+ vsyncOffsetNs = (vsyncDurationNs * VSYNC_OFFSET_PERCENTAGE) / 100;
+ } else {
+ vsyncSampler = null;
+ vsyncDurationNs = -1; // Value unused.
+ vsyncOffsetNs = -1; // Value unused.
+ }
+}
+
+void VideoFrameReleaseTimeHelper::enable() {
+ haveSync = false;
+ if (useDefaultDisplayVsync) {
+ vsyncSampler.addObserver();
+ }
+}
+
+void VideoFrameReleaseTimeHelper::disable() {
+ if (useDefaultDisplayVsync) {
+ vsyncSampler.removeObserver();
+ }
+}
+
+long VideoFrameReleaseTimeHelper::adjustReleaseTime(
+ long framePresentationTimeUs,
+ long unadjustedReleaseTimeNs) {
+ long framePresentationTimeNs = framePresentationTimeUs * 1000;
+
+ // Until we know better, the adjustment will be a no-op.
+ long adjustedFrameTimeNs = framePresentationTimeNs;
+ long adjustedReleaseTimeNs = unadjustedReleaseTimeNs;
+
+ if (haveSync) {
+ // See if we've advanced to the next frame.
+ if (framePresentationTimeUs != lastFramePresentationTimeUs) {
+ frameCount++;
+ adjustedLastFrameTimeNs = pendingAdjustedFrameTimeNs;
+ }
+ if (frameCount >= MIN_FRAMES_FOR_ADJUSTMENT) {
+ // We're synced and have waited the required number of frames to apply an
+ // adjustment. Calculate the average frame time across all the frames
+ // we've seen since the last sync. This will typically give us a frame
+ // rate at a finer granularity than the frame times themselves (which
+ // often only have millisecond granularity).
+ long averageFrameDurationNs =
+ (framePresentationTimeNs - syncFramePresentationTimeNs) / frameCount;
+ // Project the adjusted frame time forward using the average.
+ long candidateAdjustedFrameTimeNs =
+ adjustedLastFrameTimeNs + averageFrameDurationNs;
+
+ if (isDriftTooLarge(candidateAdjustedFrameTimeNs,
+ unadjustedReleaseTimeNs)) {
+ haveSync = false;
+ } else {
+ adjustedFrameTimeNs = candidateAdjustedFrameTimeNs;
+ adjustedReleaseTimeNs = syncUnadjustedReleaseTimeNs +
+ adjustedFrameTimeNs -
+ syncFramePresentationTimeNs;
+ }
+ } else {
+ // We're synced but haven't waited the required number of frames to apply
+ // an adjustment. Check drift anyway.
+ if (isDriftTooLarge(framePresentationTimeNs, unadjustedReleaseTimeNs)) {
+ haveSync = false;
+ }
+ }
+ }
+
+ // If we need to sync, do so now.
+ if (!haveSync) {
+ syncFramePresentationTimeNs = framePresentationTimeNs;
+ syncUnadjustedReleaseTimeNs = unadjustedReleaseTimeNs;
+ frameCount = 0;
+ haveSync = true;
+ onSynced();
+ }
+
+ lastFramePresentationTimeUs = framePresentationTimeUs;
+ pendingAdjustedFrameTimeNs = adjustedFrameTimeNs;
+
+ if (vsyncSampler == null || vsyncSampler.sampledVsyncTimeNs == 0) {
+ return adjustedReleaseTimeNs;
+ }
+
+ // Find the timestamp of the closest vsync. This is the vsync that we're
+ // targeting.
+ long snappedTimeNs = closestVsync(
+ adjustedReleaseTimeNs, vsyncSampler.sampledVsyncTimeNs, vsyncDurationNs);
+ // Apply an offset so that we release before the target vsync, but after the
+ // previous one.
+ return snappedTimeNs - vsyncOffsetNs;
+}
+
+void VideoFrameReleaseTimeHelper::onSynced() {
+ // Do nothing.
+}
+
+boolean VideoFrameReleaseTimeHelper::isDriftTooLarge(long frameTimeNs,
+ long releaseTimeNs) {
+ long elapsedFrameTimeNs = frameTimeNs - syncFramePresentationTimeNs;
+ long elapsedReleaseTimeNs = releaseTimeNs - syncUnadjustedReleaseTimeNs;
+ return Math.abs(elapsedReleaseTimeNs - elapsedFrameTimeNs) >
+ MAX_ALLOWED_DRIFT_NS;
+}
+
+static long VideoFrameReleaseTimeHelper::closestVsync(long releaseTime,
+ long sampledVsyncTime,
+ long vsyncDuration) {
+ long vsyncCount = (releaseTime - sampledVsyncTime) / vsyncDuration;
+ long snappedTimeNs = sampledVsyncTime + (vsyncDuration * vsyncCount);
+ long snappedBeforeNs;
+ long snappedAfterNs;
+ if (releaseTime <= snappedTimeNs) {
+ snappedBeforeNs = snappedTimeNs - vsyncDuration;
+ snappedAfterNs = snappedTimeNs;
+ } else {
+ snappedBeforeNs = snappedTimeNs;
+ snappedAfterNs = snappedTimeNs + vsyncDuration;
+ }
+ long snappedAfterDiff = snappedAfterNs - releaseTime;
+ long snappedBeforeDiff = releaseTime - snappedBeforeNs;
+ return snappedAfterDiff < snappedBeforeDiff ? snappedAfterNs
+ : snappedBeforeNs;
+}
+
+static double VideoFrameReleaseTimeHelper::getDefaultDisplayRefreshRate(
+ Context context) {
+ WindowManager manager =
+ (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
+ return manager.getDefaultDisplay() != null
+ ? manager.getDefaultDisplay().getRefreshRate()
+ : DISPLAY_REFRESH_RATE_UNKNOWN;
+}
+
+} // namespace shared
+} // namespace android
+} // namespace starboard
diff --git a/src/starboard/android/shared/video_frame_release_time_adjuster.h b/src/starboard/android/shared/video_frame_release_time_adjuster.h
new file mode 100644
index 0000000..a345660
--- /dev/null
+++ b/src/starboard/android/shared/video_frame_release_time_adjuster.h
@@ -0,0 +1,189 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_VIDEO_FRAME_RELEASE_TIME_ADJUSTER_H_
+#define STARBOARD_ANDROID_SHARED_VIDEO_FRAME_RELEASE_TIME_ADJUSTER_H_
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+class VideoFrameReleaseTimeAdjuster {
+ public:
+ /**
+ * Constructs an instance that smooths frame release timestamps but does not
+ * align them with the default display's vsync signal.
+ */
+ VideoFrameReleaseTimeHelper();
+
+ /**
+ * Constructs an instance that smooths frame release timestamps and aligns
+ * them with the default display's vsync signal.
+ *
+ * @param context A context from which information about the default display
+ * can be retrieved.
+ */
+ VideoFrameReleaseTimeHelper(Context context);
+
+ /** Enables the helper. */
+ void enable();
+
+ /** Disables the helper. */
+ void disable();
+
+ /**
+ * Adjusts a frame release timestamp.
+ *
+ * @param framePresentationTimeUs The frame's presentation time, in
+ * microseconds.
+ * @param unadjustedReleaseTimeNs The frame's unadjusted release time, in
+ * nanoseconds and in the same time base as {@link System#nanoTime()}.
+ * @return The adjusted frame release timestamp, in nanoseconds and in the
+ * same time base as
+ * {@link System#nanoTime()}.
+ */
+ long adjustReleaseTime(long framePresentationTimeUs,
+ long unadjustedReleaseTimeNs);
+
+ protected:
+ void onSynced();
+
+ private:
+ VideoFrameReleaseTimeHelper(double defaultDisplayRefreshRate);
+
+ boolean isDriftTooLarge(long frameTimeNs, long releaseTimeNs);
+
+ static long closestVsync(long releaseTime,
+ long sampledVsyncTime,
+ long vsyncDuration);
+
+ static double getDefaultDisplayRefreshRate(Context context);
+
+ /**
+ * Samples display vsync timestamps. A single instance using a single {@link
+ * Choreographer} is shared by all {@link VideoFrameReleaseTimeHelper}
+ * instances. This is done to avoid a resource leak in the platform on API
+ * levels prior to 23.
+ */
+ class VSyncSampler implements FrameCallback, Handler.Callback {
+ public:
+ static VSyncSampler getInstance() { return INSTANCE; }
+
+ VSyncSampler() {
+ choreographerOwnerThread =
+ new HandlerThread("ChoreographerOwner:Handler");
+ choreographerOwnerThread.start();
+ handler = new Handler(choreographerOwnerThread.getLooper(), this);
+ handler.sendEmptyMessage(CREATE_CHOREOGRAPHER);
+ }
+
+ /**
+ * Notifies the sampler that a {@link VideoFrameReleaseTimeHelper} is
+ * observing {@link #sampledVsyncTimeNs}, and hence that the value should be
+ * periodically updated.
+ */
+ void addObserver() { handler.sendEmptyMessage(MSG_ADD_OBSERVER); }
+
+ /**
+ * Notifies the sampler that a {@link VideoFrameReleaseTimeHelper} is no
+ * longer observing {@link #sampledVsyncTimeNs}.
+ */
+ void removeObserver() { handler.sendEmptyMessage(MSG_REMOVE_OBSERVER); }
+
+ void doFrame(long vsyncTimeNs) {
+ sampledVsyncTimeNs = vsyncTimeNs;
+ choreographer.postFrameCallbackDelayed(this,
+ CHOREOGRAPHER_SAMPLE_DELAY_MILLIS);
+ }
+
+ boolean handleMessage(Message message) {
+ switch (message.what) {
+ case CREATE_CHOREOGRAPHER: {
+ createChoreographerInstanceInternal();
+ return true;
+ }
+ case MSG_ADD_OBSERVER: {
+ addObserverInternal();
+ return true;
+ }
+ case MSG_REMOVE_OBSERVER: {
+ removeObserverInternal();
+ return true;
+ }
+ default: { return false; }
+ }
+ }
+
+ volatile long sampledVsyncTimeNs;
+
+ private:
+ void createChoreographerInstanceInternal() {
+ choreographer = Choreographer.getInstance();
+ }
+
+ void addObserverInternal() {
+ observerCount++;
+ if (observerCount == 1) {
+ choreographer.postFrameCallback(this);
+ }
+ }
+
+ void removeObserverInternal() {
+ observerCount--;
+ if (observerCount == 0) {
+ choreographer.removeFrameCallback(this);
+ sampledVsyncTimeNs = 0;
+ }
+ }
+
+ static final int CREATE_CHOREOGRAPHER = 0;
+ static final int MSG_ADD_OBSERVER = 1;
+ static final int MSG_REMOVE_OBSERVER = 2;
+
+ static final VSyncSampler INSTANCE = new VSyncSampler();
+
+ final Handler handler;
+ final HandlerThread choreographerOwnerThread;
+ Choreographer choreographer;
+ int observerCount;
+ };
+
+ static final double DISPLAY_REFRESH_RATE_UNKNOWN = -1;
+ static final long CHOREOGRAPHER_SAMPLE_DELAY_MILLIS = 500;
+ static final long MAX_ALLOWED_DRIFT_NS = 20000000;
+
+ static final long VSYNC_OFFSET_PERCENTAGE = 80;
+ static final int MIN_FRAMES_FOR_ADJUSTMENT = 6;
+ static final long NANOS_PER_SECOND = 1000000000L;
+
+ final VSyncSampler vsyncSampler;
+ final boolean useDefaultDisplayVsync;
+ final long vsyncDurationNs;
+ final long vsyncOffsetNs;
+
+ long lastFramePresentationTimeUs;
+ long adjustedLastFrameTimeNs;
+ long pendingAdjustedFrameTimeNs;
+
+ boolean haveSync;
+ long syncUnadjustedReleaseTimeNs;
+ long syncFramePresentationTimeNs;
+ long frameCount;
+};
+
+} // namespace shared
+} // namespace android
+} // namespace starboard
+
+#endif // STARBOARD_ANDROID_SHARED_VIDEO_FRAME_RELEASE_TIME_ADJUSTER_H_
diff --git a/src/starboard/android/shared/video_render_algorithm.cc b/src/starboard/android/shared/video_render_algorithm.cc
new file mode 100644
index 0000000..639ae01
--- /dev/null
+++ b/src/starboard/android/shared/video_render_algorithm.cc
@@ -0,0 +1,124 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/video_render_algorithm.h"
+
+#include <algorithm>
+
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/android/shared/media_common.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+namespace {
+
+const SbTimeMonotonic kBufferTooLateThreshold = -30 * kSbTimeMillisecond;
+const SbTimeMonotonic kBufferReadyThreshold = 50 * kSbTimeMillisecond;
+
+jlong GetSystemNanoTime() {
+ timespec now;
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ return now.tv_sec * 1000000000LL + now.tv_nsec;
+}
+
+} // namespace
+
+void VideoRenderAlgorithm::Render(
+ MediaTimeProvider* media_time_provider,
+ std::list<scoped_refptr<VideoFrame>>* frames,
+ VideoRendererSink::DrawFrameCB draw_frame_cb) {
+ SB_DCHECK(media_time_provider);
+ SB_DCHECK(frames);
+ SB_DCHECK(draw_frame_cb);
+
+ while (frames->size() > 0) {
+ if (frames->front()->is_end_of_stream()) {
+ frames->pop_front();
+ SB_DCHECK(frames->empty())
+ << "Expected end of stream output buffer to be the last buffer.";
+ break;
+ }
+
+ bool is_audio_playing;
+ bool is_audio_eos_played;
+ bool is_underflow;
+ SbTime playback_time = media_time_provider->GetCurrentMediaTime(
+ &is_audio_playing, &is_audio_eos_played, &is_underflow);
+ if (!is_audio_playing) {
+ break;
+ }
+
+ jlong early_us = frames->front()->timestamp() - playback_time;
+
+ auto system_time_ns = GetSystemNanoTime();
+ auto unadjusted_frame_release_time_ns =
+ system_time_ns + (early_us * kSbTimeNanosecondsPerMicrosecond);
+
+ auto adjusted_release_time_ns =
+ video_frame_release_time_helper_.AdjustReleaseTime(
+ frames->front()->timestamp(), unadjusted_frame_release_time_ns);
+
+ early_us = (adjusted_release_time_ns - system_time_ns) /
+ kSbTimeNanosecondsPerMicrosecond;
+
+ if (early_us < kBufferTooLateThreshold) {
+ frames->pop_front();
+ ++dropped_frames_;
+ } else if (early_us < kBufferReadyThreshold) {
+ auto status = draw_frame_cb(frames->front(), adjusted_release_time_ns);
+ SB_DCHECK(status == VideoRendererSink::kReleased);
+ frames->pop_front();
+ } else {
+ break;
+ }
+ }
+}
+
+VideoRenderAlgorithm::VideoFrameReleaseTimeHelper::
+ VideoFrameReleaseTimeHelper() {
+ auto* env = JniEnvExt::Get();
+ ScopedLocalJavaRef<jobject> j_context(env->CallStarboardObjectMethodOrAbort(
+ "getApplicationContext", "()Landroid/content/Context;"));
+ j_video_frame_release_time_helper_ =
+ env->NewObjectOrAbort("dev/cobalt/media/VideoFrameReleaseTimeHelper",
+ "(Landroid/content/Context;)V", j_context.Get());
+ j_video_frame_release_time_helper_ =
+ env->ConvertLocalRefToGlobalRef(j_video_frame_release_time_helper_);
+ env->CallVoidMethod(j_video_frame_release_time_helper_, "enable", "()V");
+}
+
+VideoRenderAlgorithm::VideoFrameReleaseTimeHelper::
+ ~VideoFrameReleaseTimeHelper() {
+ SB_DCHECK(j_video_frame_release_time_helper_);
+ auto* env = JniEnvExt::Get();
+ env->CallVoidMethod(j_video_frame_release_time_helper_, "disable", "()V");
+ env->DeleteGlobalRef(j_video_frame_release_time_helper_);
+ j_video_frame_release_time_helper_ = nullptr;
+}
+
+jlong VideoRenderAlgorithm::VideoFrameReleaseTimeHelper::AdjustReleaseTime(
+ jlong frame_presentation_time_us,
+ jlong unadjusted_release_time_ns) {
+ SB_DCHECK(j_video_frame_release_time_helper_);
+ auto* env = JniEnvExt::Get();
+ return env->CallLongMethodOrAbort(
+ j_video_frame_release_time_helper_, "adjustReleaseTime", "(JJ)J",
+ frame_presentation_time_us, unadjusted_release_time_ns);
+}
+
+} // namespace shared
+} // namespace android
+} // namespace starboard
diff --git a/src/starboard/android/shared/video_render_algorithm.h b/src/starboard/android/shared/video_render_algorithm.h
new file mode 100644
index 0000000..927b514
--- /dev/null
+++ b/src/starboard/android/shared/video_render_algorithm.h
@@ -0,0 +1,55 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_VIDEO_RENDER_ALGORITHM_H_
+#define STARBOARD_ANDROID_SHARED_VIDEO_RENDER_ALGORITHM_H_
+
+#include <list>
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/shared/starboard/player/filter/video_renderer_internal.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+class VideoRenderAlgorithm : public ::starboard::shared::starboard::player::
+ filter::VideoRenderAlgorithm {
+ public:
+ void Render(MediaTimeProvider* media_time_provider,
+ std::list<scoped_refptr<VideoFrame>>* frames,
+ VideoRendererSink::DrawFrameCB draw_frame_cb) override;
+ int GetDroppedFrames() override { return dropped_frames_; }
+
+ private:
+ class VideoFrameReleaseTimeHelper {
+ public:
+ VideoFrameReleaseTimeHelper();
+ ~VideoFrameReleaseTimeHelper();
+ jlong AdjustReleaseTime(jlong frame_presentation_time_us,
+ jlong unadjusted_release_time_ns);
+
+ private:
+ jobject j_video_frame_release_time_helper_ = nullptr;
+ };
+
+ VideoFrameReleaseTimeHelper video_frame_release_time_helper_;
+ int dropped_frames_ = 0;
+};
+
+} // namespace shared
+} // namespace android
+} // namespace starboard
+
+#endif // STARBOARD_ANDROID_SHARED_VIDEO_RENDER_ALGORITHM_H_
diff --git a/src/starboard/android/shared/video_window.cc b/src/starboard/android/shared/video_window.cc
new file mode 100644
index 0000000..da4bbf0
--- /dev/null
+++ b/src/starboard/android/shared/video_window.cc
@@ -0,0 +1,207 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/video_window.h"
+
+#include <android/native_window.h>
+#include <android/native_window_jni.h>
+#include <EGL/egl.h>
+#include <GLES2/gl2.h>
+#include <jni.h>
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/condition_variable.h"
+#include "starboard/configuration.h"
+#include "starboard/log.h"
+#include "starboard/shared/gles/gl_call.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+namespace {
+
+// Global pointer to the single video surface.
+jobject g_j_video_surface = NULL;
+// Global pointer to the single video window.
+ANativeWindow* g_native_video_window = NULL;
+
+// Facilitate synchronization of punch-out videos.
+SbMutex g_bounds_updates_mutex = SB_MUTEX_INITIALIZER;
+SbConditionVariable g_bounds_updates_condition =
+ SB_CONDITION_VARIABLE_INITIALIZER;
+int g_bounds_updates_needed = 0;
+int g_bounds_updates_scheduled = 0;
+
+} // namespace
+
+extern "C" SB_EXPORT_PLATFORM void
+Java_dev_cobalt_media_VideoSurfaceView_nativeOnVideoSurfaceChanged(
+ JNIEnv* env,
+ jobject unused_this,
+ jobject surface) {
+ if (g_j_video_surface) {
+ // TODO: Ensure that the decoder isn't still using the surface.
+ env->DeleteGlobalRef(g_j_video_surface);
+ }
+ if (g_native_video_window) {
+ // TODO: Ensure that the decoder isn't still using the window.
+ ANativeWindow_release(g_native_video_window);
+ }
+ if (surface) {
+ g_j_video_surface = env->NewGlobalRef(surface);
+ g_native_video_window = ANativeWindow_fromSurface(env, surface);
+ } else {
+ g_j_video_surface = NULL;
+ g_native_video_window = NULL;
+ }
+}
+
+extern "C" SB_EXPORT_PLATFORM void
+Java_dev_cobalt_media_VideoSurfaceView_nativeOnLayoutNeeded() {
+ SbMutexAcquire(&g_bounds_updates_mutex);
+ ++g_bounds_updates_needed;
+ SbMutexRelease(&g_bounds_updates_mutex);
+}
+
+extern "C" SB_EXPORT_PLATFORM void
+Java_dev_cobalt_media_VideoSurfaceView_nativeOnLayoutScheduled() {
+ SbMutexAcquire(&g_bounds_updates_mutex);
+ ++g_bounds_updates_scheduled;
+ SbMutexRelease(&g_bounds_updates_mutex);
+}
+
+extern "C" SB_EXPORT_PLATFORM void
+Java_dev_cobalt_media_VideoSurfaceView_nativeOnGlobalLayout() {
+ SbMutexAcquire(&g_bounds_updates_mutex);
+ g_bounds_updates_needed -= g_bounds_updates_scheduled;
+ g_bounds_updates_scheduled = 0;
+ if (g_bounds_updates_needed <= 0) {
+ g_bounds_updates_needed = 0;
+ SbConditionVariableSignal(&g_bounds_updates_condition);
+ }
+ SbMutexRelease(&g_bounds_updates_mutex);
+}
+
+jobject GetVideoSurface() {
+ return g_j_video_surface;
+}
+
+ANativeWindow* GetVideoWindow() {
+ return g_native_video_window;
+}
+
+void ClearVideoWindow() {
+ if (!g_native_video_window) {
+ SB_LOG(INFO) << "Tried to clear video window when it was null.";
+ return;
+ }
+
+ EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
+ eglInitialize(display, NULL, NULL);
+ if (display == EGL_NO_DISPLAY) {
+ SB_DLOG(ERROR) << "Found no EGL display in ClearVideoWindow";
+ return;
+ }
+
+ const EGLint kAttributeList[] = {
+ EGL_RED_SIZE,
+ 8,
+ EGL_GREEN_SIZE,
+ 8,
+ EGL_BLUE_SIZE,
+ 8,
+ EGL_ALPHA_SIZE,
+ 8,
+ EGL_RENDERABLE_TYPE,
+ EGL_OPENGL_ES2_BIT,
+ EGL_NONE,
+ 0,
+ EGL_NONE,
+ };
+
+ // First, query how many configs match the given attribute list.
+ EGLint num_configs = 0;
+ EGL_CALL(eglChooseConfig(display, kAttributeList, NULL, 0, &num_configs));
+ SB_DCHECK(num_configs != 0);
+
+ // Allocate space to receive the matching configs and retrieve them.
+ EGLConfig* configs = new EGLConfig[num_configs];
+ EGL_CALL(eglChooseConfig(display, kAttributeList, configs, num_configs,
+ &num_configs));
+
+ EGLNativeWindowType native_window =
+ static_cast<EGLNativeWindowType>(g_native_video_window);
+ EGLConfig config;
+
+ // Find the first config that successfully allow a window surface to be
+ // created.
+ EGLSurface surface;
+ for (int config_number = 0; config_number < num_configs; ++config_number) {
+ config = configs[config_number];
+ surface = eglCreateWindowSurface(display, config, native_window, NULL);
+ if (eglGetError() == EGL_SUCCESS)
+ break;
+ }
+ if (surface == EGL_NO_SURFACE) {
+ SB_DLOG(ERROR) << "Found no EGL surface in ClearVideoWindow";
+ return;
+ }
+ SB_DCHECK(surface != EGL_NO_SURFACE);
+
+ delete[] configs;
+
+ // Create an OpenGL ES 2.0 context.
+ EGLContext context = EGL_NO_CONTEXT;
+ EGLint context_attrib_list[] = {
+ EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE,
+ };
+ context =
+ eglCreateContext(display, config, EGL_NO_CONTEXT, context_attrib_list);
+ SB_DCHECK(eglGetError() == EGL_SUCCESS);
+ SB_DCHECK(context != EGL_NO_CONTEXT);
+
+ /* connect the context to the surface */
+ EGL_CALL(eglMakeCurrent(display, surface, surface, context));
+
+ GL_CALL(glClearColor(0, 0, 0, 1));
+ GL_CALL(glClear(GL_COLOR_BUFFER_BIT));
+ GL_CALL(glFlush());
+ EGL_CALL(eglSwapBuffers(display, surface));
+
+ // Cleanup all used resources.
+ EGL_CALL(
+ eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT));
+ EGL_CALL(eglDestroyContext(display, context));
+ EGL_CALL(eglDestroySurface(display, surface));
+ EGL_CALL(eglTerminate(display));
+}
+
+void WaitForVideoBoundsUpdate() {
+ SbMutexAcquire(&g_bounds_updates_mutex);
+ if (g_bounds_updates_needed > 0) {
+ // Use a timed wait to deal with possible race conditions in which
+ // suspend occurs in the middle of a bounds update.
+ SbConditionVariableWaitTimed(&g_bounds_updates_condition,
+ &g_bounds_updates_mutex,
+ 100 * kSbTimeMillisecond);
+ g_bounds_updates_needed = 0;
+ g_bounds_updates_scheduled = 0;
+ }
+ SbMutexRelease(&g_bounds_updates_mutex);
+}
+
+} // namespace shared
+} // namespace android
+} // namespace starboard
diff --git a/src/starboard/android/shared/video_window.h b/src/starboard/android/shared/video_window.h
new file mode 100644
index 0000000..39cb33f
--- /dev/null
+++ b/src/starboard/android/shared/video_window.h
@@ -0,0 +1,43 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_VIDEO_WINDOW_H_
+#define STARBOARD_ANDROID_SHARED_VIDEO_WINDOW_H_
+
+#include <android/native_window.h>
+#include <jni.h>
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+// Returns the surface which video should be rendered. This is the surface
+// that owns the native window returned by |GetVideoWindow|.
+jobject GetVideoSurface();
+
+// Returns the native window into which video should be rendered.
+ANativeWindow* GetVideoWindow();
+
+// Clear the video window by painting it Black. This function is safe to call
+// regardless of whether the video window has been initialized or not.
+void ClearVideoWindow();
+
+// Wait for all outstanding adjustments of video bounds before returning.
+void WaitForVideoBoundsUpdate();
+
+} // namespace shared
+} // namespace android
+} // namespace starboard
+
+#endif // STARBOARD_ANDROID_SHARED_VIDEO_WINDOW_H_
diff --git a/src/starboard/android/shared/window_blur_on_screen_keyboard.cc b/src/starboard/android/shared/window_blur_on_screen_keyboard.cc
new file mode 100644
index 0000000..fe84c62
--- /dev/null
+++ b/src/starboard/android/shared/window_blur_on_screen_keyboard.cc
@@ -0,0 +1,22 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/window.h"
+
+#if SB_HAS(ON_SCREEN_KEYBOARD)
+void SbWindowBlurOnScreenKeyboard(SbWindow window, int ticket) {
+ // Stub.
+ return;
+}
+#endif // SB_HAS(ON_SCREEN_KEYBOARD)
diff --git a/src/starboard/android/shared/window_create.cc b/src/starboard/android/shared/window_create.cc
new file mode 100644
index 0000000..ec3ecf7
--- /dev/null
+++ b/src/starboard/android/shared/window_create.cc
@@ -0,0 +1,22 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/window.h"
+
+#include "starboard/android/shared/application_android.h"
+
+SbWindow SbWindowCreate(const SbWindowOptions* options) {
+ return starboard::android::shared::ApplicationAndroid::Get()->CreateWindow(
+ options);
+}
diff --git a/src/starboard/android/shared/window_destroy.cc b/src/starboard/android/shared/window_destroy.cc
new file mode 100644
index 0000000..afb0c7e
--- /dev/null
+++ b/src/starboard/android/shared/window_destroy.cc
@@ -0,0 +1,22 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/window.h"
+
+#include "starboard/android/shared/application_android.h"
+
+bool SbWindowDestroy(SbWindow window) {
+ return starboard::android::shared::ApplicationAndroid::Get()->DestroyWindow(
+ window);
+}
diff --git a/src/starboard/android/shared/window_focus_on_screen_keyboard.cc b/src/starboard/android/shared/window_focus_on_screen_keyboard.cc
new file mode 100644
index 0000000..00dea03
--- /dev/null
+++ b/src/starboard/android/shared/window_focus_on_screen_keyboard.cc
@@ -0,0 +1,22 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/window.h"
+
+#if SB_HAS(ON_SCREEN_KEYBOARD)
+void SbWindowFocusOnScreenKeyboard(SbWindow window, int ticket) {
+ // Stub.
+ return;
+}
+#endif // SB_HAS(ON_SCREEN_KEYBOARD)
diff --git a/src/starboard/android/shared/window_get_on_screen_keyboard_bounding_rect.cc b/src/starboard/android/shared/window_get_on_screen_keyboard_bounding_rect.cc
new file mode 100644
index 0000000..90bcf2f
--- /dev/null
+++ b/src/starboard/android/shared/window_get_on_screen_keyboard_bounding_rect.cc
@@ -0,0 +1,23 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/window.h"
+
+#if SB_HAS(ON_SCREEN_KEYBOARD)
+bool SbWindowGetOnScreenKeyboardBoundingRect(SbWindow window,
+ SbWindowRect* bounding_rect) {
+ // Stub.
+ return true;
+}
+#endif // SB_HAS(ON_SCREEN_KEYBOARD)
diff --git a/src/starboard/android/shared/window_get_platform_handle.cc b/src/starboard/android/shared/window_get_platform_handle.cc
new file mode 100644
index 0000000..60fd357
--- /dev/null
+++ b/src/starboard/android/shared/window_get_platform_handle.cc
@@ -0,0 +1,25 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/window.h"
+
+#include "starboard/android/shared/window_internal.h"
+
+void* SbWindowGetPlatformHandle(SbWindow window) {
+ if (!SbWindowIsValid(window)) {
+ return NULL;
+ }
+ // EGLNativeWindowType and ANativeWindow* are the same.
+ return window->native_window;
+}
diff --git a/src/starboard/android/shared/window_get_size.cc b/src/starboard/android/shared/window_get_size.cc
new file mode 100644
index 0000000..c4bbebd
--- /dev/null
+++ b/src/starboard/android/shared/window_get_size.cc
@@ -0,0 +1,51 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <android/native_window.h>
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/android/shared/window_internal.h"
+#include "starboard/log.h"
+#include "starboard/window.h"
+
+using starboard::android::shared::JniEnvExt;
+using starboard::android::shared::ScopedLocalJavaRef;
+
+bool SbWindowGetSize(SbWindow window, SbWindowSize* size) {
+ if (!SbWindowIsValid(window)) {
+ SB_DLOG(ERROR) << __FUNCTION__ << ": Invalid window.";
+ return false;
+ }
+
+ size->width = ANativeWindow_getWidth(window->native_window);
+ size->height = ANativeWindow_getHeight(window->native_window);
+
+ JniEnvExt* env = JniEnvExt::Get();
+ ScopedLocalJavaRef<jobject> display_size(
+ env->CallStarboardObjectMethodOrAbort("getDisplaySize",
+ "()Landroid/util/Size;"));
+ int display_width =
+ env->CallIntMethodOrAbort(display_size.Get(), "getWidth", "()I");
+ int display_height =
+ env->CallIntMethodOrAbort(display_size.Get(), "getHeight", "()I");
+
+ // In the off chance we have non-square pixels, use the max ratio so the
+ // highest quality video suitable to the device gets selected.
+ size->video_pixel_ratio = std::max(
+ static_cast<float>(display_width) / size->width,
+ static_cast<float>(display_height) / size->height);
+
+ return true;
+}
diff --git a/src/starboard/android/shared/window_hide_on_screen_keyboard.cc b/src/starboard/android/shared/window_hide_on_screen_keyboard.cc
new file mode 100644
index 0000000..4ecb6e8
--- /dev/null
+++ b/src/starboard/android/shared/window_hide_on_screen_keyboard.cc
@@ -0,0 +1,25 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/window.h"
+
+#include "starboard/android/shared/application_android.h"
+
+#if SB_HAS(ON_SCREEN_KEYBOARD)
+void SbWindowHideOnScreenKeyboard(SbWindow window, int ticket) {
+ starboard::android::shared::ApplicationAndroid::Get()
+ ->SbWindowHideOnScreenKeyboard(window, ticket);
+ return;
+}
+#endif // SB_HAS(ON_SCREEN_KEYBOARD)
diff --git a/src/starboard/android/shared/window_internal.h b/src/starboard/android/shared/window_internal.h
new file mode 100644
index 0000000..24a6ed7
--- /dev/null
+++ b/src/starboard/android/shared/window_internal.h
@@ -0,0 +1,26 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_WINDOW_INTERNAL_H_
+#define STARBOARD_ANDROID_SHARED_WINDOW_INTERNAL_H_
+
+#include <android/native_window.h>
+
+#include "starboard/window.h"
+
+struct SbWindowPrivate {
+ ANativeWindow* native_window;
+};
+
+#endif // STARBOARD_ANDROID_SHARED_WINDOW_INTERNAL_H_
diff --git a/src/starboard/android/shared/window_is_on_screen_keyboard_shown.cc b/src/starboard/android/shared/window_is_on_screen_keyboard_shown.cc
new file mode 100644
index 0000000..711f85c
--- /dev/null
+++ b/src/starboard/android/shared/window_is_on_screen_keyboard_shown.cc
@@ -0,0 +1,22 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/window.h"
+
+#if SB_HAS(ON_SCREEN_KEYBOARD)
+bool SbWindowIsOnScreenKeyboardShown(SbWindow window) {
+ // Stub.
+ return true;
+}
+#endif // SB_HAS(ON_SCREEN_KEYBOARD)
diff --git a/src/starboard/android/shared/window_set_on_screen_keyboard_keep_focus.cc b/src/starboard/android/shared/window_set_on_screen_keyboard_keep_focus.cc
new file mode 100644
index 0000000..fd38256
--- /dev/null
+++ b/src/starboard/android/shared/window_set_on_screen_keyboard_keep_focus.cc
@@ -0,0 +1,22 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/window.h"
+
+#if SB_HAS(ON_SCREEN_KEYBOARD)
+void SbWindowSetOnScreenKeyboardKeepFocus(SbWindow window, bool keep_focus) {
+ // Stub.
+ return;
+}
+#endif // SB_HAS(ON_SCREEN_KEYBOARD)
diff --git a/src/starboard/android/shared/window_show_on_screen_keyboard.cc b/src/starboard/android/shared/window_show_on_screen_keyboard.cc
new file mode 100644
index 0000000..d5be7c8
--- /dev/null
+++ b/src/starboard/android/shared/window_show_on_screen_keyboard.cc
@@ -0,0 +1,27 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/window.h"
+
+#include "starboard/android/shared/application_android.h"
+
+#if SB_HAS(ON_SCREEN_KEYBOARD)
+void SbWindowShowOnScreenKeyboard(SbWindow window,
+ const char* input_text,
+ int ticket) {
+ starboard::android::shared::ApplicationAndroid::Get()
+ ->SbWindowShowOnScreenKeyboard(window, input_text, ticket);
+ return;
+}
+#endif // SB_HAS(ON_SCREEN_KEYBOARD)
diff --git a/src/starboard/android/x86/atomic_public.h b/src/starboard/android/x86/atomic_public.h
new file mode 100644
index 0000000..640dc99
--- /dev/null
+++ b/src/starboard/android/x86/atomic_public.h
@@ -0,0 +1,15 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/atomic_public.h"
diff --git a/src/starboard/android/x86/configuration_public.h b/src/starboard/android/x86/configuration_public.h
new file mode 100644
index 0000000..e36dc6a
--- /dev/null
+++ b/src/starboard/android/x86/configuration_public.h
@@ -0,0 +1,22 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// The Starboard configuration for Android X86. Other devices will have
+// specific Starboard implementations, even if they ultimately are running some
+// version of Android.
+
+// Other source files should never include this header directly, but should
+// include the generic "starboard/configuration.h" instead.
+
+#include "starboard/android/shared/configuration_public.h"
diff --git a/src/starboard/android/x86/gyp_configuration.gypi b/src/starboard/android/x86/gyp_configuration.gypi
new file mode 100644
index 0000000..3c5d162
--- /dev/null
+++ b/src/starboard/android/x86/gyp_configuration.gypi
@@ -0,0 +1,41 @@
+# Copyright 2016 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+{
+ 'variables': {
+ 'target_arch': 'ia32',
+ },
+
+ 'target_defaults': {
+ 'default_configuration': 'android-x86_debug',
+ 'configurations': {
+ 'android-x86_debug': {
+ 'inherit_from': ['debug_base'],
+ },
+ 'android-x86_devel': {
+ 'inherit_from': ['devel_base'],
+ },
+ 'android-x86_qa': {
+ 'inherit_from': ['qa_base'],
+ },
+ 'android-x86_gold': {
+ 'inherit_from': ['gold_base'],
+ },
+ }, # end of configurations
+ },
+
+ 'includes': [
+ '../shared/gyp_configuration.gypi',
+ ],
+}
diff --git a/src/starboard/android/x86/gyp_configuration.py b/src/starboard/android/x86/gyp_configuration.py
new file mode 100644
index 0000000..f31ab4b
--- /dev/null
+++ b/src/starboard/android/x86/gyp_configuration.py
@@ -0,0 +1,20 @@
+# Copyright 2016 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Starboard Android x86 platform build configuration."""
+
+from starboard.android.shared import gyp_configuration as shared_configuration
+
+
+def CreatePlatformConfig():
+ return shared_configuration.AndroidConfiguration('android-x86', 'x86')
diff --git a/src/starboard/android/x86/starboard_platform.gyp b/src/starboard/android/x86/starboard_platform.gyp
new file mode 100644
index 0000000..7e4cdc2
--- /dev/null
+++ b/src/starboard/android/x86/starboard_platform.gyp
@@ -0,0 +1,16 @@
+# Copyright 2016 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+{
+ 'includes': [ '../shared/starboard_platform.gypi' ],
+}
diff --git a/src/starboard/android/x86/starboard_platform_tests.gyp b/src/starboard/android/x86/starboard_platform_tests.gyp
new file mode 100644
index 0000000..f7f55a3
--- /dev/null
+++ b/src/starboard/android/x86/starboard_platform_tests.gyp
@@ -0,0 +1,16 @@
+# Copyright 2016 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+{
+ 'includes': [ '../shared/starboard_platform_tests.gypi' ],
+}
diff --git a/src/starboard/android/x86/thread_types_public.h b/src/starboard/android/x86/thread_types_public.h
new file mode 100644
index 0000000..1416655
--- /dev/null
+++ b/src/starboard/android/x86/thread_types_public.h
@@ -0,0 +1,15 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/thread_types_public.h"
diff --git a/src/starboard/build/win_symlink.py b/src/starboard/build/win_symlink.py
index cf1832f..b343132 100644
--- a/src/starboard/build/win_symlink.py
+++ b/src/starboard/build/win_symlink.py
@@ -29,12 +29,10 @@
build 14972, which is not widely available yet.
"""
-import os
-import shutil
-import subprocess
-import stat
-import time
-import traceback
+
+################################################################################
+# API #
+################################################################################
def CreateReparsePoint(from_folder, link_folder):
@@ -64,9 +62,17 @@
return _RmtreeShallow(dirpath)
-#####################
-# Implementation
-#####################
+################################################################################
+# IMPL #
+################################################################################
+
+
+import os
+import shutil
+import subprocess
+import stat
+import time
+import traceback
def _RemoveEmptyDirectory(path):
@@ -89,7 +95,7 @@
subprocess.check_output(['cmd', '/c', 'rmdir', '/S', '/Q', path])
-def _ReadReparsePoint(path):
+def _ReadReparsePointShell(path):
path = os.path.abspath(path)
cmd_parts = ['fsutil', 'reparsepoint', 'query', path]
try:
@@ -110,8 +116,25 @@
return None
+def _ReadReparsePoint(path):
+ try:
+ from win_symlink_fast import FastReadReparseLink
+ return FastReadReparseLink(path)
+ except Exception as err:
+ # Fallback
+ print(__file__ + ' error: ' + str(err) + \
+ ', falling back to command line version.')
+ return _ReadReparsePointShell(path)
+
+
def _IsReparsePoint(path):
- return None != _ReadReparsePoint(path)
+ try:
+ from win_symlink_fast import FastIsReparseLink
+ return FastIsReparseLink(path)
+ except Exception as err:
+ print(__file__ + ' error: ' + str(err) + \
+ ', falling back to command line version.')
+ return None != _ReadReparsePointShell(path)
def _CreateReparsePoint(from_folder, link_folder):
@@ -121,12 +144,17 @@
_RemoveEmptyDirectory(link_folder)
else:
_UnlinkReparsePoint(link_folder) # Deletes if it exists.
-
- par_dir = os.path.dirname(link_folder)
- if not os.path.isdir(par_dir):
- os.makedirs(par_dir)
- cmd_parts = ['cmd', '/c', 'mklink', '/j', link_folder, from_folder]
- subprocess.check_output(cmd_parts)
+ try:
+ from win_symlink_fast import FastCreateReparseLink
+ FastCreateReparseLink(from_folder, link_folder)
+ except Exception as err:
+ print(__file__ + ' error: ' + str(err) + \
+ ', falling back to command line version.')
+ par_dir = os.path.dirname(link_folder)
+ if not os.path.isdir(par_dir):
+ os.makedirs(par_dir)
+ cmd_parts = ['cmd', '/c', 'mklink', '/j', link_folder, from_folder]
+ subprocess.check_output(cmd_parts)
diff --git a/src/starboard/build/win_symlink_fast.py b/src/starboard/build/win_symlink_fast.py
new file mode 100644
index 0000000..0fd76ef
--- /dev/null
+++ b/src/starboard/build/win_symlink_fast.py
@@ -0,0 +1,195 @@
+#!/usr/bin/python
+# Copyright 2019 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+################################################################################
+# API #
+################################################################################
+
+
+def FastIsReparseLink(path):
+ return _FastIsReparseLink(path)
+
+
+def FastReadReparseLink(path):
+ return _FastReadReparseLink(path)
+
+
+def FastCreateReparseLink(from_folder, link_folder):
+ return _FastCreateReparseLink(from_folder, link_folder)
+
+
+################################################################################
+# IMPL #
+################################################################################
+
+
+from ctypes import \
+ POINTER, c_buffer, byref, addressof, c_ubyte, Structure, Union
+from ctypes.wintypes import \
+ DWORD, LPCWSTR, HANDLE, LPVOID, BOOL, USHORT, ULONG, WCHAR, WinError, WinDLL
+
+
+kernel32 = WinDLL('kernel32')
+LPDWORD = POINTER(DWORD)
+UCHAR = c_ubyte
+
+
+GetFileAttributesW = kernel32.GetFileAttributesW
+GetFileAttributesW.restype = DWORD
+GetFileAttributesW.argtypes = (LPCWSTR,) #lpFileName In
+
+
+INVALID_FILE_ATTRIBUTES = 0xFFFFFFFF
+FILE_ATTRIBUTE_REPARSE_POINT = 0x00400
+
+
+CreateFileW = kernel32.CreateFileW
+CreateFileW.restype = HANDLE
+CreateFileW.argtypes = (LPCWSTR, # lpFileName In
+ DWORD, # dwDesiredAccess In
+ DWORD, # dwShareMode In
+ LPVOID, # lpSecurityAttributes In_opt
+ DWORD, # dwCreationDisposition In
+ DWORD, # dwFlagsAndAttributes In
+ HANDLE) # hTemplateFile In_opt
+
+
+CloseHandle = kernel32.CloseHandle
+CloseHandle.restype = BOOL
+CloseHandle.argtypes = (HANDLE,) #hObject In
+
+
+INVALID_HANDLE_VALUE = HANDLE(-1).value
+OPEN_EXISTING = 3
+FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
+FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000
+
+
+DeviceIoControl = kernel32.DeviceIoControl
+DeviceIoControl.restype = BOOL
+DeviceIoControl.argtypes = (HANDLE, #hDevice In
+ DWORD, #dwIoControlCode In
+ LPVOID, #lpInBuffer In_opt
+ DWORD, #nInBufferSize In
+ LPVOID, #lpOutBuffer Out_opt
+ DWORD, #nOutBufferSize In
+ LPDWORD, #lpBytesReturned Out_opt
+ LPVOID) #lpOverlapped Inout_opt
+
+
+FSCTL_GET_REPARSE_POINT = 0x000900A8
+IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003
+IO_REPARSE_TAG_SYMLINK = 0xA000000C
+MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 0x4000
+SYMBOLIC_LINK_FLAG_DIRECTORY = 0x1
+SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE = 0x2
+
+class GENERIC_REPARSE_BUFFER(Structure):
+ _fields_ = (('DataBuffer', UCHAR * 1),)
+
+
+class SYMBOLIC_LINK_REPARSE_BUFFER(Structure):
+ _fields_ = (('SubstituteNameOffset', USHORT),
+ ('SubstituteNameLength', USHORT),
+ ('PrintNameOffset', USHORT),
+ ('PrintNameLength', USHORT),
+ ('Flags', ULONG),
+ ('PathBuffer', WCHAR * 1))
+ @property
+ def print_name(self):
+ arrayt = WCHAR * (self.PrintNameLength // 2)
+ offset = type(self).PathBuffer.offset + self.PrintNameOffset
+ return arrayt.from_address(addressof(self) + offset).value
+
+
+class MOUNT_POINT_REPARSE_BUFFER(Structure):
+ _fields_ = (('SubstituteNameOffset', USHORT),
+ ('SubstituteNameLength', USHORT),
+ ('PrintNameOffset', USHORT),
+ ('PrintNameLength', USHORT),
+ ('PathBuffer', WCHAR * 1))
+ @property
+ def print_name(self):
+ arrayt = WCHAR * (self.PrintNameLength // 2)
+ offset = type(self).PathBuffer.offset + self.PrintNameOffset
+ return arrayt.from_address(addressof(self) + offset).value
+
+
+class REPARSE_DATA_BUFFER(Structure):
+ class REPARSE_BUFFER(Union):
+ _fields_ = (('SymbolicLinkReparseBuffer',
+ SYMBOLIC_LINK_REPARSE_BUFFER),
+ ('MountPointReparseBuffer',
+ MOUNT_POINT_REPARSE_BUFFER),
+ ('GenericReparseBuffer',
+ GENERIC_REPARSE_BUFFER))
+ _fields_ = (('ReparseTag', ULONG),
+ ('ReparseDataLength', USHORT),
+ ('Reserved', USHORT),
+ ('ReparseBuffer', REPARSE_BUFFER))
+ _anonymous_ = ('ReparseBuffer',)
+
+
+def _ToUnicode(s):
+ return s.decode('utf-8')
+
+
+def _FastCreateReparseLink(from_folder, link_folder):
+ from_folder = _ToUnicode(from_folder)
+ link_folder = _ToUnicode(link_folder)
+ from win32file import CreateSymbolicLink
+ # Only supported from Windows 10 Insiders build 14972
+ flags = SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE | \
+ SYMBOLIC_LINK_FLAG_DIRECTORY
+ CreateSymbolicLink(link_folder,from_folder, flags)
+
+
+def _FastIsReparseLink(path):
+ path = _ToUnicode(path)
+ result = GetFileAttributesW(path)
+ if result == INVALID_FILE_ATTRIBUTES:
+ return False
+ return bool(result & FILE_ATTRIBUTE_REPARSE_POINT)
+
+
+def _FastReadReparseLink(path):
+ path = _ToUnicode(path)
+ reparse_point_handle = CreateFileW(path,
+ 0,
+ 0,
+ None,
+ OPEN_EXISTING,
+ FILE_FLAG_OPEN_REPARSE_POINT |
+ FILE_FLAG_BACKUP_SEMANTICS,
+ None)
+ if reparse_point_handle == INVALID_HANDLE_VALUE:
+ return None
+ target_buffer = c_buffer(MAXIMUM_REPARSE_DATA_BUFFER_SIZE)
+ n_bytes_returned = DWORD()
+ io_result = DeviceIoControl(reparse_point_handle,
+ FSCTL_GET_REPARSE_POINT,
+ None, 0,
+ target_buffer, len(target_buffer),
+ byref(n_bytes_returned),
+ None)
+ CloseHandle(reparse_point_handle)
+ if not io_result:
+ return None
+ rdb = REPARSE_DATA_BUFFER.from_buffer(target_buffer)
+ if rdb.ReparseTag == IO_REPARSE_TAG_SYMLINK:
+ return rdb.SymbolicLinkReparseBuffer.print_name
+ elif rdb.ReparseTag == IO_REPARSE_TAG_MOUNT_POINT:
+ return rdb.MountPointReparseBuffer.print_name
+ return None
diff --git a/src/starboard/client_porting/poem/stdlib_poem.h b/src/starboard/client_porting/poem/stdlib_poem.h
index 342857b..6764227 100644
--- a/src/starboard/client_porting/poem/stdlib_poem.h
+++ b/src/starboard/client_porting/poem/stdlib_poem.h
@@ -46,6 +46,8 @@
// number conversion functions
#undef strtol
#define strtol(s, o, b) SbStringParseSignedInteger(s, o, b)
+#undef strtoll
+#define strtoll(s, o, b) SbStringParseSignedInteger(s, o, b)
#undef atoi
#define atoi(v) SbStringAToI(v)
#undef atol
diff --git a/src/starboard/common/new.cc b/src/starboard/common/new.cc
index b407c8a..cf320a6 100644
--- a/src/starboard/common/new.cc
+++ b/src/starboard/common/new.cc
@@ -15,6 +15,7 @@
// TODO: I believe this code will have to be linked into all DLLs on
// Windows. I think Starboard can do this through GYP when the time comes.
+#include <new>
#include "starboard/memory.h"
void* operator new(size_t size) {
@@ -25,6 +26,14 @@
SbMemoryDeallocate(pointer);
}
+void* operator new (size_t size, const std::nothrow_t& nothrow_tag) {
+ return SbMemoryAllocate(size);
+}
+
+void operator delete (void* pointer, const std::nothrow_t& nothrow_tag) {
+ SbMemoryDeallocate(pointer);
+}
+
void* operator new[](size_t size) {
return SbMemoryAllocate(size);
}
diff --git a/src/starboard/configuration.h b/src/starboard/configuration.h
index 063bbbc..cc81bf2 100644
--- a/src/starboard/configuration.h
+++ b/src/starboard/configuration.h
@@ -110,6 +110,10 @@
// Add support for SbThreadSampler and SbThreadContext to support profiling.
#define SB_THREAD_SAMPLER_VERSION SB_EXPERIMENTAL_API_VERSION
+// Introduce a new API in starboard/window.h which declares the function
+// SbWindowUpdateOnScreenKeyboardSuggestions().
+#define SB_ON_SCREEN_KEYBOARD_SUGGESTIONS_VERSION SB_EXPERIMENTAL_API_VERSION
+
// --- Release Candidate Feature Defines -------------------------------------
// --- Common Detected Features ----------------------------------------------
diff --git a/src/starboard/event.h b/src/starboard/event.h
index ea09448..f2001a2 100644
--- a/src/starboard/event.h
+++ b/src/starboard/event.h
@@ -258,6 +258,18 @@
// kSbEventOnScreenKeyboardInvalidTicket.
kSbEventTypeOnScreenKeyboardBlurred,
+#if SB_API_VERSION >= SB_ON_SCREEN_KEYBOARD_SUGGESTIONS_VERSION
+ // The platform has updated the on screen keyboard suggestions. This event is
+ // triggered by the system or by the application's OnScreenKeyboard update
+ // suggestions method. The event has int data representing a ticket. The
+ // ticket is used by the application to mark individual calls to the update
+ // suggestions method as successfully completed. Events triggered by the
+ // application have tickets passed in via
+ // SbWindowUpdateOnScreenKeyboardSuggestions. System-triggered events have
+ // ticket value kSbEventOnScreenKeyboardInvalidTicket.
+ kSbEventTypeOnScreenKeyboardSuggestionsUpdated,
+#endif // SB_ON_SCREEN_KEYBOARD_SUGGESTIONS_VERSION
+
#endif // SB_HAS(ON_SCREEN_KEYBOARD)
#if SB_HAS(CAPTIONS)
// One or more of the fields returned by SbAccessibilityGetCaptionSettings
diff --git a/src/starboard/linux/shared/gyp_configuration.gypi b/src/starboard/linux/shared/gyp_configuration.gypi
index 5706563..332d785 100644
--- a/src/starboard/linux/shared/gyp_configuration.gypi
+++ b/src/starboard/linux/shared/gyp_configuration.gypi
@@ -43,30 +43,6 @@
'linker_flags': [
'-static-libstdc++'
],
-
- 'conditions': [
- ['use_dlmalloc_allocator==1 and use_asan==0', {
- 'linker_flags': [
- # If we're not using the system allocator (e.g. we are using dlmalloc
- # and ASAN is inactive) then we should never be making any calls to
- # malloc() or free(). The following linker flags ensure that they
- # are not linked in because we don't actually implement the wrapped
- # version of them. We do link them in when using ASAN, as it needs to
- # use its own version of these allocators in the Starboard
- # implementation.
- '-Wl,--wrap=malloc',
- '-Wl,--wrap=calloc',
- '-Wl,--wrap=realloc',
- '-Wl,--wrap=memalign',
- '-Wl,--wrap=reallocalign',
- '-Wl,--wrap=free',
- '-Wl,--wrap=strdup',
- '-Wl,--wrap=malloc_usable_size',
- '-Wl,--wrap=malloc_stats_fast',
- '-Wl,--wrap=__cxa_demangle',
- ],
- }],
- ],
},
'target_defaults': {
diff --git a/src/starboard/linux/x64x11/dlmalloc/atomic_public.h b/src/starboard/linux/x64x11/dlmalloc/atomic_public.h
new file mode 100644
index 0000000..0d5b249
--- /dev/null
+++ b/src/starboard/linux/x64x11/dlmalloc/atomic_public.h
@@ -0,0 +1,20 @@
+// Copyright 2019 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_LINUX_X64X11_DLMALLOC_ATOMIC_PUBLIC_H_
+#define STARBOARD_LINUX_X64X11_DLMALLOC_ATOMIC_PUBLIC_H_
+
+#include "starboard/linux/shared/atomic_public.h"
+
+#endif // STARBOARD_LINUX_X64X11_DLMALLOC_ATOMIC_PUBLIC_H_
diff --git a/src/starboard/linux/x64x11/dlmalloc/configuration_public.h b/src/starboard/linux/x64x11/dlmalloc/configuration_public.h
new file mode 100644
index 0000000..0b81e15
--- /dev/null
+++ b/src/starboard/linux/x64x11/dlmalloc/configuration_public.h
@@ -0,0 +1,21 @@
+// Copyright 2019 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_LINUX_X64X11_DLMALLOC_CONFIGURATION_PUBLIC_H_
+#define STARBOARD_LINUX_X64X11_DLMALLOC_CONFIGURATION_PUBLIC_H_
+
+// Include the X64X11 Linux configuration.
+#include "starboard/linux/x64x11/configuration_public.h"
+
+#endif // STARBOARD_LINUX_X64X11_DLMALLOC_CONFIGURATION_PUBLIC_H_
diff --git a/src/starboard/linux/x64x11/dlmalloc/gyp_configuration.gypi b/src/starboard/linux/x64x11/dlmalloc/gyp_configuration.gypi
new file mode 100644
index 0000000..bd7aa50
--- /dev/null
+++ b/src/starboard/linux/x64x11/dlmalloc/gyp_configuration.gypi
@@ -0,0 +1,46 @@
+# Copyright 2019 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+{
+ 'target_defaults': {
+ 'default_configuration': 'linux-x64x11-dlmalloc_debug',
+ 'configurations': {
+ 'linux-x64x11-dlmalloc_debug': {
+ 'inherit_from': ['debug_base'],
+ },
+ 'linux-x64x11-dlmalloc_devel': {
+ 'inherit_from': ['devel_base'],
+ },
+ 'linux-x64x11-dlmalloc_qa': {
+ 'inherit_from': ['qa_base'],
+ },
+ 'linux-x64x11-dlmalloc_gold': {
+ 'inherit_from': ['gold_base'],
+ },
+ }, # end of configurations
+ },
+
+ 'includes': [
+ '../gyp_configuration.gypi',
+ ],
+
+ 'variables': {
+ 'use_dlmalloc_allocator': 1,
+
+ 'linker_flags': [
+ # We want to see all the places using malloc(), free(), etc.
+ '-Wl,-error-limit=0',
+ ],
+ },
+}
diff --git a/src/starboard/linux/x64x11/dlmalloc/gyp_configuration.py b/src/starboard/linux/x64x11/dlmalloc/gyp_configuration.py
new file mode 100644
index 0000000..0edf71a
--- /dev/null
+++ b/src/starboard/linux/x64x11/dlmalloc/gyp_configuration.py
@@ -0,0 +1,33 @@
+# Copyright 2019 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Starboard Linux X64 X11 dlmalloc platform configuration."""
+
+from starboard.linux.x64x11 import gyp_configuration as linux_configuration
+
+
+class LinuxX64X11DlmallocConfiguration(
+ linux_configuration.LinuxX64X11Configuration):
+ """Starboard Linux X64 X11 dlmalloc platform configuration."""
+
+ def GetVariables(self, config_name):
+ variables = super(LinuxX64X11DlmallocConfiguration,
+ self).GetVariables(config_name)
+ variables.update({
+ 'use_asan': 0,
+ })
+ return variables
+
+
+def CreatePlatformConfig():
+ return LinuxX64X11DlmallocConfiguration('linux-x64x11-dlmalloc')
diff --git a/src/starboard/linux/x64x11/dlmalloc/starboard_platform.gyp b/src/starboard/linux/x64x11/dlmalloc/starboard_platform.gyp
new file mode 100644
index 0000000..760e06b
--- /dev/null
+++ b/src/starboard/linux/x64x11/dlmalloc/starboard_platform.gyp
@@ -0,0 +1,18 @@
+# Copyright 2019 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+{
+ 'includes': [
+ '../shared/starboard_platform_target.gypi',
+ ],
+}
diff --git a/src/starboard/linux/x64x11/dlmalloc/starboard_platform_tests.gyp b/src/starboard/linux/x64x11/dlmalloc/starboard_platform_tests.gyp
new file mode 100644
index 0000000..74835fe
--- /dev/null
+++ b/src/starboard/linux/x64x11/dlmalloc/starboard_platform_tests.gyp
@@ -0,0 +1,18 @@
+# Copyright 2019 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+{
+ 'includes': [
+ '<(DEPTH)/starboard/linux/shared/starboard_platform_tests.gypi',
+ ],
+}
diff --git a/src/starboard/linux/x64x11/dlmalloc/thread_types_public.h b/src/starboard/linux/x64x11/dlmalloc/thread_types_public.h
new file mode 100644
index 0000000..a780cfd
--- /dev/null
+++ b/src/starboard/linux/x64x11/dlmalloc/thread_types_public.h
@@ -0,0 +1,20 @@
+// Copyright 2019 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_LINUX_X64X11_DLMALLOC_THREAD_TYPES_PUBLIC_H_
+#define STARBOARD_LINUX_X64X11_DLMALLOC_THREAD_TYPES_PUBLIC_H_
+
+#include "starboard/linux/shared/thread_types_public.h"
+
+#endif // STARBOARD_LINUX_X64X11_DLMALLOC_THREAD_TYPES_PUBLIC_H_
diff --git a/src/starboard/nplb/memory_reporter_test.cc b/src/starboard/nplb/memory_reporter_test.cc
index 7efb69c..ac82879 100644
--- a/src/starboard/nplb/memory_reporter_test.cc
+++ b/src/starboard/nplb/memory_reporter_test.cc
@@ -88,6 +88,16 @@
ASSERT_FALSE(A); \
}
+// A structure that cannot be allocated because it throws an exception in its
+// constructor. This is needed to test the std::nothrow version of delete since
+// it is only called when the std::nothrow version of new fails.
+struct ThrowConstructor {
+ // ThrowConstructor() throw(std::exception) { throw std::exception(); }
+ ThrowConstructor() : foo_(1) { throw std::exception(); }
+ // Required to prevent the constructor from being inlined and optimized away.
+ volatile int foo_;
+};
+
///////////////////////////////////////////////////////////////////////////////
// A memory reporter that is used to watch allocations from the system.
class TestMemReporter {
@@ -348,7 +358,7 @@
}
#endif // SB_HAS(MMAP)
-// Tests the assumption that the operator/delete will report
+// Tests the assumption that the operator new/delete will report
// memory allocations.
TEST_F(MemoryReportingTest, CapturesOperatorNewDelete) {
if (!MemoryReportingEnabled()) {
@@ -371,6 +381,59 @@
EXPECT_EQ_NO_TRACKING(my_int, mem_reporter()->last_deallocation());
}
+// Tests the assumption that the nothrow version of operator new will report
+// memory allocations.
+TEST_F(MemoryReportingTest, CapturesOperatorNewNothrow) {
+ if (!MemoryReportingEnabled()) {
+ SbLog(kSbLogPriorityInfo, "Memory reporting is disabled.\n");
+ return;
+ }
+ EXPECT_EQ_NO_TRACKING(0, mem_reporter()->number_allocs());
+ int* my_int = new (std::nothrow) int();
+ EXPECT_EQ_NO_TRACKING(1, mem_reporter()->number_allocs());
+
+ bool is_last_allocation =
+ my_int == mem_reporter()->last_allocation();
+
+ EXPECT_TRUE_NO_TRACKING(is_last_allocation);
+
+ delete my_int;
+ EXPECT_EQ_NO_TRACKING(0, mem_reporter()->number_allocs());
+
+ // Expect last deallocation to be the expected pointer.
+ EXPECT_EQ_NO_TRACKING(my_int, mem_reporter()->last_deallocation());
+}
+
+// Tests the assumption that the nothrow version of operator delete will report
+// memory deallocations.
+TEST_F(MemoryReportingTest, CapturesOperatorDeleteNothrow) {
+ if (!MemoryReportingEnabled()) {
+ SbLog(kSbLogPriorityInfo, "Memory reporting is disabled.\n");
+ return;
+ }
+ const void* init_alloc = mem_reporter()->last_allocation();
+
+ EXPECT_EQ_NO_TRACKING(0, mem_reporter()->number_allocs());
+ void* my_obj = nullptr;
+ bool caught_exception = false;
+ try {
+ my_obj = new (std::nothrow) ThrowConstructor();
+ } catch (std::exception e) {
+ caught_exception = true;
+ }
+ EXPECT_TRUE(caught_exception);
+ EXPECT_EQ_NO_TRACKING(0, mem_reporter()->number_allocs());
+
+ // Expect that an allocation occurred, even though we never got a pointer.
+ EXPECT_EQ_NO_TRACKING(nullptr, my_obj);
+ EXPECT_NE_NO_TRACKING(nullptr, mem_reporter()->last_allocation());
+ EXPECT_NE_NO_TRACKING(init_alloc, mem_reporter()->last_allocation());
+
+ // Expect last deallocation to be the allocation we never got.
+ EXPECT_EQ_NO_TRACKING(mem_reporter()->last_allocation(),
+ mem_reporter()->last_deallocation());
+}
+
#else // !defined(STARBOARD_ALLOWS_MEMORY_TRACKING)
// These are NEGATIVE tests, which test the expectation that when the
// STARBOARD_ALLOWS_MEMORY_TRACKING is undefined that the memory tracker does
diff --git a/src/starboard/nplb/nplb.gyp b/src/starboard/nplb/nplb.gyp
index ed3e4f2..807e7b1 100644
--- a/src/starboard/nplb/nplb.gyp
+++ b/src/starboard/nplb/nplb.gyp
@@ -21,6 +21,16 @@
'msvs_disabled_warnings': [4100, 4189, 4456],
'target_name': 'nplb',
'type': '<(gtest_target_type)',
+ # Enable exceptions to test nothrow delete operator.
+ 'cflags_cc!': ['-fno-exceptions' ],
+ 'cflags_cc': ['-fexceptions' ],
+ 'msvs_settings': {
+ 'VCCLCompilerTool': {
+ 'AdditionalOptions': [
+ '/EHsc', # C++ exceptions
+ ],
+ },
+ },
'sources': [
'<(DEPTH)/starboard/common/test_main.cc',
'accessibility_get_setting_test.cc',
@@ -292,6 +302,15 @@
'player_output_mode_supported_test.cc',
'url_player_create_test.cc',
],
+ 'conditions': [
+ ['gl_type != "none"', {
+ 'dependencies': [
+ # This is needed because SbPlayerTest depends on
+ # FakeGraphicsContextProvider which depends on EGL and GLES.
+ '<(DEPTH)/starboard/egl_and_gles/egl_and_gles.gyp:egl_and_gles',
+ ],
+ }],
+ ],
}],
],
},
diff --git a/src/starboard/shared/opus/opus_audio_decoder.cc b/src/starboard/shared/opus/opus_audio_decoder.cc
index dda6c11..850edb3 100644
--- a/src/starboard/shared/opus/opus_audio_decoder.cc
+++ b/src/starboard/shared/opus/opus_audio_decoder.cc
@@ -98,9 +98,13 @@
// TODO: Consider fill it with silence.
SB_LOG(ERROR) << kDecodeFunctionName
<< "() failed with error code: " << decoded_frames;
+#if SB_HAS(PLAYER_ERROR_MESSAGE)
error_cb_(kSbPlayerErrorDecode,
FormatString("%s() failed with error code: %d",
kDecodeFunctionName, decoded_frames));
+#else // SB_HAS(PLAYER_ERROR_MESSAGE)
+ error_cb_();
+#endif // SB_HAS(PLAYER_ERROR_MESSAGE)
return;
}
diff --git a/src/starboard/shared/starboard/player/filter/stub_video_decoder.cc b/src/starboard/shared/starboard/player/filter/stub_video_decoder.cc
index 31093b2..8a331a6 100644
--- a/src/starboard/shared/starboard/player/filter/stub_video_decoder.cc
+++ b/src/starboard/shared/starboard/player/filter/stub_video_decoder.cc
@@ -23,7 +23,8 @@
void StubVideoDecoder::Initialize(const DecoderStatusCB& decoder_status_cb,
const ErrorCB& error_cb) {
SB_UNREFERENCED_PARAMETER(error_cb);
- SB_DCHECK(decoder_status_cb_);
+ SB_DCHECK(decoder_status_cb);
+ SB_DCHECK(!decoder_status_cb_);
decoder_status_cb_ = decoder_status_cb;
}
@@ -48,6 +49,7 @@
void StubVideoDecoder::WriteEndOfStream() {
decoder_status_cb_(kBufferFull, VideoFrame::CreateEOSFrame());
}
+
void StubVideoDecoder::Reset() {}
SbDecodeTarget StubVideoDecoder::GetCurrentDecodeTarget() {
diff --git a/src/starboard/shared/starboard/player/filter/testing/player_filter_tests.gyp b/src/starboard/shared/starboard/player/filter/testing/player_filter_tests.gyp
index b6545c4..96772c2 100644
--- a/src/starboard/shared/starboard/player/filter/testing/player_filter_tests.gyp
+++ b/src/starboard/shared/starboard/player/filter/testing/player_filter_tests.gyp
@@ -30,6 +30,15 @@
'<(DEPTH)/starboard/testing/fake_graphics_context_provider.cc',
'<(DEPTH)/starboard/testing/fake_graphics_context_provider.h',
],
+ 'conditions': [
+ ['gl_type != "none"', {
+ 'dependencies': [
+ # This is needed because VideoDecoderTest depends on
+ # FakeGraphicsContextProvider which depends on EGL and GLES.
+ '<(DEPTH)/starboard/egl_and_gles/egl_and_gles.gyp:egl_and_gles',
+ ],
+ }],
+ ],
'defines': [
# This allows the tests to include internal only header files.
'STARBOARD_IMPLEMENTATION',
@@ -48,13 +57,15 @@
'variables': {
'content_test_input_files': ['<!@(python <(DEPTH)/starboard/build/list_dmp_files.py "starboard/shared/starboard/player/testdata")'],
'content_test_output_subdir': 'starboard/shared/starboard/player/testdata',
+ 'depot_tools_path': ['<!@(python <(DEPTH)/build/find_depot_tools_escaped.py)'],
},
'actions' : [
{
# This action requires depot_tools to be in path
# (https://cobalt.googlesource.com/depot_tools).
'action_name': 'player_filter_tests_download_test_data',
- 'action': [ 'download_from_google_storage',
+ 'action': [ 'python',
+ '<(depot_tools_path)/download_from_google_storage.py',
'--no_resume',
'--no_auth',
'--num_threads', '8',
diff --git a/src/starboard/shared/starboard/player/job_thread.cc b/src/starboard/shared/starboard/player/job_thread.cc
index 97c335c..9ee3d90 100644
--- a/src/starboard/shared/starboard/player/job_thread.cc
+++ b/src/starboard/shared/starboard/player/job_thread.cc
@@ -33,10 +33,12 @@
} // namespace
-JobThread::JobThread(const char* thread_name) {
+JobThread::JobThread(const char* thread_name,
+ int64_t stack_size,
+ SbThreadPriority priority) {
ThreadParam thread_param(this);
thread_ =
- SbThreadCreate(0, kSbThreadPriorityNormal, kSbThreadNoAffinity, true,
+ SbThreadCreate(stack_size, priority, kSbThreadNoAffinity, true,
thread_name, &JobThread::ThreadEntryPoint, &thread_param);
SB_DCHECK(SbThreadIsValid(thread_));
ScopedLock scoped_lock(thread_param.mutex);
diff --git a/src/starboard/shared/starboard/player/job_thread.h b/src/starboard/shared/starboard/player/job_thread.h
index 0138d8a..f4a5263 100644
--- a/src/starboard/shared/starboard/player/job_thread.h
+++ b/src/starboard/shared/starboard/player/job_thread.h
@@ -32,7 +32,9 @@
// This class implements a thread that holds a JobQueue.
class JobThread {
public:
- explicit JobThread(const char* thread_name);
+ explicit JobThread(const char* thread_name,
+ int64_t stack_size = 0,
+ SbThreadPriority priority = kSbThreadPriorityNormal);
~JobThread();
JobQueue* job_queue() { return job_queue_.get(); }
diff --git a/src/starboard/shared/starboard/player/player_create.cc b/src/starboard/shared/starboard/player/player_create.cc
index 329d014..e0ebf3f 100644
--- a/src/starboard/shared/starboard/player/player_create.cc
+++ b/src/starboard/shared/starboard/player/player_create.cc
@@ -33,10 +33,6 @@
FilterBasedPlayerWorkerHandler;
using starboard::shared::starboard::player::PlayerWorker;
-#if SB_HAS(PLAYER_WITH_URL)
-// No implementation : use SbPlayerCreateWithUrl instead.
-#else
-
SbPlayer SbPlayerCreate(SbWindow window,
SbMediaVideoCodec video_codec,
SbMediaAudioCodec audio_codec,
@@ -134,5 +130,3 @@
return player;
}
-
-#endif // SB_HAS(PLAYER_WITH_URL)
diff --git a/src/starboard/shared/starboard/player/player_output_mode_supported.cc b/src/starboard/shared/starboard/player/player_output_mode_supported.cc
index 83e3504..9691d3a 100644
--- a/src/starboard/shared/starboard/player/player_output_mode_supported.cc
+++ b/src/starboard/shared/starboard/player/player_output_mode_supported.cc
@@ -18,15 +18,6 @@
#include "starboard/log.h"
#include "starboard/shared/starboard/player/filter/video_decoder_internal.h"
-#if SB_HAS(PLAYER_WITH_URL)
-
-bool SbPlayerOutputModeSupportedWithUrl(SbPlayerOutputMode output_mode) {
- // Stub.
- return false;
-}
-
-#else
-
bool SbPlayerOutputModeSupported(SbPlayerOutputMode output_mode,
SbMediaVideoCodec codec,
SbDrmSystem drm_system) {
@@ -34,4 +25,3 @@
OutputModeSupported(output_mode, codec, drm_system);
}
-#endif // SB_HAS(PLAYER_WITH_URL)
diff --git a/src/starboard/shared/starboard/player/testdata/google_glass_h264_aac.dmp b/src/starboard/shared/starboard/player/testdata/google_glass_h264_aac.dmp
deleted file mode 100644
index 4b4d33c..0000000
--- a/src/starboard/shared/starboard/player/testdata/google_glass_h264_aac.dmp
+++ /dev/null
Binary files differ
diff --git a/src/starboard/shared/starboard/player/testdata/google_glass_vp9_opus.dmp b/src/starboard/shared/starboard/player/testdata/google_glass_vp9_opus.dmp
deleted file mode 100644
index 801f498..0000000
--- a/src/starboard/shared/starboard/player/testdata/google_glass_vp9_opus.dmp
+++ /dev/null
Binary files differ
diff --git a/src/starboard/shared/starboard/player/video_dmp_common.cc b/src/starboard/shared/starboard/player/video_dmp_common.cc
index 4a8ca82..1f215f5 100644
--- a/src/starboard/shared/starboard/player/video_dmp_common.cc
+++ b/src/starboard/shared/starboard/player/video_dmp_common.cc
@@ -104,7 +104,8 @@
audio_header->audio_specific_config =
audio_header->stored_audio_specific_config.data();
#else
- SB_DCHECK(8 >= audio_header->stored_audio_specific_config.size());
+ SB_DCHECK(sizeof(audio_header->audio_specific_config) >=
+ audio_header->stored_audio_specific_config.size());
SbMemoryCopy(audio_header->audio_specific_config,
audio_header->stored_audio_specific_config.data(),
audio_header->stored_audio_specific_config.size());
diff --git a/src/starboard/tools/app_launcher_packager.py b/src/starboard/tools/app_launcher_packager.py
index 3ad3528..514b662 100644
--- a/src/starboard/tools/app_launcher_packager.py
+++ b/src/starboard/tools/app_launcher_packager.py
@@ -21,6 +21,36 @@
that the app launcher can be run independent of the Cobalt source tree.
"""
+
+################################################################################
+# API #
+################################################################################
+
+
+def CopyAppLauncherTools(repo_root, dest_root, additional_sub_dirs=[]):
+ """Copies app launcher related files to the destination root.
+ repo_root: The 'src' path that will be used for packaging.
+ dest_root: The directory where the src files will be stored.
+ additional_sub_dirs: Some platforms may need to include certain dependencies
+ beyond the default include directories. Each element in this list will be
+ a path with assumed root at src/. For example ['third_party/X'] would
+ include path 'src/third_party/X/**/*.py' in the packaging operation. For a
+ list of default paths see _PYTHON_SRC_DIRS in this file."""
+ _CopyAppLauncherTools(repo_root, dest_root, additional_sub_dirs)
+
+
+def MakeZipArchive(src, output_zip):
+ """Convenience function to zip up all files in the src directory (produced
+ as dest_root argument in CopyAppLauncherTools()) which will create a zip
+ file with the relative root being the src directory."""
+ _MakeZipArchive(src, output_zip)
+
+
+################################################################################
+# IMPL #
+################################################################################
+
+
import argparse
import logging
import os
@@ -31,8 +61,19 @@
from paths import REPOSITORY_ROOT
from paths import THIRD_PARTY_ROOT
sys.path.append(THIRD_PARTY_ROOT)
-import jinja2
import starboard.tools.platform
+import jinja2
+
+
+# Default python directories to find code.
+_PYTHON_SRC_DIRS = [
+ 'starboard',
+ 'cobalt', # TODO: Test and possibly prune.
+ 'buildbot', # Needed because of device_server.
+ 'lbshell', # TODO: Test and possibly prune.
+ 'third_party/jinja2', # Required by this app_launcher_packager.py script.
+ 'third_party/markupsafe' # Required by third_party/jinja2
+]
def _MakeDir(d):
@@ -58,15 +99,7 @@
return out_dir in d
-def CopyPythonFiles(source_root, dest_root):
- """Copy all python files to the destination folder.
-
- Copy from source to destination while maintaining the directory structure.
-
- Args:
- source_root: Absolute path to the root of files to be copied.
- dest_root: Destination into which files will be copied.
- """
+def _FindPythonAndCertFiles(source_root):
logging.info('Searching in ' + source_root + ' for python files.')
file_list = []
for root, _, files in os.walk(source_root):
@@ -74,19 +107,12 @@
if _IsOutDir(source_root, root):
continue
for f in files:
- if f.endswith('.py'):
- source_file = os.path.join(root, f)
- dest_file = source_file.replace(source_root, dest_root)
- file_list.append((source_file, dest_file))
-
- logging.info('Starting copy of ' + str(len(file_list)) + ' python files.')
- for (source, dest) in file_list:
- _MakeDir(os.path.dirname(dest))
- shutil.copy2(source, dest)
- logging.info('Copy of python files finished.')
+ if f.endswith('.py') or f.endswith('.crt') or f.endswith('.key'):
+ file_list.append(os.path.join(root, f))
+ return file_list
-def WritePlatformsInfo(repo_root, dest_root):
+def _WritePlatformsInfo(repo_root, dest_root):
"""Get platforms' information and write the platform.py based on a template.
Platform.py is responsible for enumerating all supported platforms in the
@@ -105,13 +131,11 @@
current_file = os.path.abspath(__file__)
current_dir = os.path.dirname(current_file)
dest_dir = current_dir.replace(repo_root, dest_root)
-
platforms_map = {}
for p in starboard.tools.platform.GetAll():
platform_path = os.path.relpath(
starboard.tools.platform.Get(p).path, repo_root)
platforms_map[p] = platform_path
-
template = jinja2.Template(
open(os.path.join(current_dir, 'platform.py.template')).read())
with open(os.path.join(dest_dir, 'platform.py'), 'w+') as f:
@@ -119,26 +143,44 @@
logging.info('Finished baking in platform info files.')
-def CopyAppLauncherTools(repo_root, dest_root):
+def _CopyAppLauncherTools(repo_root, dest_root, additional_sub_dirs):
+ # Step 1: Remove previous output directory if it exists
if os.path.isdir(dest_root):
shutil.rmtree(dest_root)
+ # Step 2: Find all python files from specified search directories.
+ subdirs = _PYTHON_SRC_DIRS + additional_sub_dirs
+ copy_list = []
+ for d in subdirs:
+ flist = _FindPythonAndCertFiles(os.path.join(repo_root, d))
+ copy_list.extend(flist)
+ # Copy all src/*.py from repo_root without recursing down.
+ for f in os.listdir(repo_root):
+ src = os.path.join(repo_root, f)
+ if os.path.isfile(src) and src.endswith('.py'):
+ copy_list.append(src)
+ # Order by file path string and remove any duplicate paths.
+ copy_list = list(set(copy_list))
+ copy_list.sort()
+ # Step 3: Copy the src files to the destination directory.
+ for src in copy_list:
+ tail_path = os.path.relpath(src, repo_root)
+ dst = os.path.join(dest_root, tail_path)
+ d = os.path.dirname(dst)
+ if not os.path.isdir(d):
+ os.makedirs(d)
+ logging.info(src + ' -> ' + dst)
+ shutil.copy2(src, dst)
+ # Step 4: Re-write the platform infos file in the new repo copy.
+ _WritePlatformsInfo(repo_root, dest_root)
- CopyPythonFiles(repo_root, dest_root)
- WritePlatformsInfo(repo_root, dest_root)
- # Create an extra zip file of the app launcher package so that users have
- # the option of downloading a single file which is much faster, especially
- # on x20.
- logging.info('Creating a zip file of the app launcher package.')
-
- # Make a zip that has the same name as the dest_root. Then the zip file
- # and dest_root are guaranteed to be on the same file system under the
- # same parent, so that moving the zip file to dest_root is optimized.
- app_launcher_zip_file = shutil.make_archive(dest_root, 'zip', dest_root)
- dest_zip = os.path.join(dest_root, 'app_launcher.zip')
- if os.path.isfile(dest_zip):
- os.unlink(dest_zip)
- shutil.move(app_launcher_zip_file, dest_zip)
+def _MakeZipArchive(src, output_zip):
+ if os.path.isfile(output_zip):
+ os.unlink(output_zip)
+ logging.info('Creating a zip file of the app launcher package')
+ logging.info(src + ' -> ' + output_zip)
+ tmp_file = shutil.make_archive(src, 'zip', src)
+ shutil.move(tmp_file, output_zip)
def main(command_args):
diff --git a/src/starboard/window.h b/src/starboard/window.h
index 373248d..3b883b5 100644
--- a/src/starboard/window.h
+++ b/src/starboard/window.h
@@ -205,6 +205,19 @@
// is not showing does nothing and does not fire any event.
SB_EXPORT void SbWindowBlurOnScreenKeyboard(SbWindow window, int ticket);
+#if SB_API_VERSION >= SB_ON_SCREEN_KEYBOARD_SUGGESTIONS_VERSION
+// Update the on-screen keyboard custom suggestions. Fire
+// kSbEventTypeOnScreenKeyboardSuggestionsUpdated.
+// kSbEventTypeOnScreenKeyboardSuggestionsUpdated has data |ticket|. The
+// suggestions should remain up-to-date when the keyboard is shown after being
+// hidden.
+SB_EXPORT void SbWindowUpdateOnScreenKeyboardSuggestions(
+ SbWindow window,
+ const char* suggestions[],
+ int num_suggestions,
+ int ticket);
+#endif // SB_API_VERSION >= SB_ON_SCREEN_KEYBOARD_SUGGESTIONS_VERSION
+
#endif // SB_HAS(ON_SCREEN_KEYBOARD)
#ifdef __cplusplus