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/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'