upload android base code part7

This commit is contained in:
August 2018-08-08 18:09:17 +08:00
parent 4e516ec6ed
commit 841ae54672
25229 changed files with 1709508 additions and 0 deletions

View file

@ -0,0 +1,245 @@
/*
* Copyright 2013 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.
*/
// The SampleGenPlugin source is in the buildSrc directory.
import com.example.android.samples.build.SampleGenPlugin
apply plugin: SampleGenPlugin
// Add a preflight task that depends on the "refresh" task that gets
// added by the SampleGenPlugin.
task preflight {
project.afterEvaluate({preflight.dependsOn(project.refresh)})
}
task wrapper(type: Wrapper) {
gradleVersion = '2.10'
}
String outPath(String buildType) {
/*
def repoInfo = "repo info platform/developers/build".execute().text
def buildPath = (repoInfo =~ /Mount path: (.*)/)[0][1]
*/
return "${samplegen.pathToBuild}/out/${buildType}/${samplegen.targetSampleName()}";
}
/**
* Collapse a path "IntelliJ-style" by putting dots rather than slashes between
* path components that have only one child. So the two paths
*
* com/example/android/foo/bar.java
* com/example/android/bar/foo.java
*
* Become
* com.example.android/foo/bar.java
* com.example.android/bar/foo.java
*
* @param path
* @param roots
* @return
*/
Map<String,String> collapsePaths(FileTree path, List<String> roots) {
Map result = new HashMap<String,String>();
println ("******************** Collapse *************************")
path.visit { FileVisitDetails f ->
if (f.isDirectory()) return;
StringBuilder collapsedPath = new StringBuilder("${f.name}");
File current = f.file;
//
// Starting at this file, walk back to the root of the path and
// substitute dots for any directory that has only one child.
//
// Don't substitute a dot for the separator between the end of the
// path and the filename, even if there's only one file in the directory.
if (!f.isDirectory()) {
current = current.parentFile;
collapsedPath.insert(0, "${current.name}/")
}
// For everything else, use a dot if there's only one child and
// a slash otherwise. Filter out the root paths, too--we only want
// the relative path. But wait, Groovy/Gradle is capricious and
// won't return the proper value from a call to roots.contains(String)!
// I'm using roots.sum here instead of tracking down why a list of
// strings can't return true from contains() when given a string that
// it quite obviously does contain.
current = current.parentFile;
while((current != null)
&& (roots.sum {String r-> return r.equals(current.absolutePath) ? 1 : 0 } == 0)) {
char separator = current.list().length > 1 ? '/' : '.';
collapsedPath.insert(0, "${current.name}${separator}");
current = current.parentFile;
}
result.put(f.file.path, collapsedPath.toString());
}
println ("******************** Collapse results *********************")
result.each {entry -> println("- ${entry}");}
return result
}
task emitAnt(type:Copy) {
def outputPath = outPath("ant");
def inputPath = "${project.projectDir}/${samplegen.targetSampleModule()}"
into outputPath
includeEmptyDirs
["main", "common", "template"].each { input ->
[[ "java", "src"], ["res", "res"]].each { filetype ->
def srcPath = "${inputPath}/src/${input}/${filetype[0]}"
into("${filetype[1]}") {
from(srcPath)
}
}
}
from("${inputPath}/src/main") { include "AndroidManifest.xml" }
from("${inputPath}/src/template") { include "project.properties" }
}
task emitGradle(type:Copy) {
dependsOn(preflight)
def outputPath = outPath("gradle")
def inputPath = "${project.projectDir}"
// Copy entire sample into output -- since it's already in Gradle format, we'll explicitly exclude content that
// doesn't belong here.
into outputPath
from("${inputPath}") {
// Paths to exclude from output
exclude ".gradle"
exclude "_index.jd"
exclude "bin"
exclude "buildSrc"
exclude "local.properties"
exclude "template-params.xml"
exclude "*.iml"
exclude "**/.idea"
exclude "**/build"
exclude "**/proguard-project.txt"
exclude "${samplegen.targetSampleModule()}/**/README*.txt"
exclude "**/README-*.txt"
// src directory needs to be consolidated, will be done in next section
exclude "${samplegen.targetSampleModule()}/src/"
}
// Consolidate source directories
["main", "common", "template"].each { input ->
["java", "res", "assets", "rs"].each { filetype ->
def srcPath = "${inputPath}/${samplegen.targetSampleModule()}/src/${input}/${filetype}"
into("${samplegen.targetSampleModule()}/src/main/${filetype}") {
from(srcPath)
}
}
}
// Copy AndroidManifest.xml
into ("${samplegen.targetSampleModule()}/src/main") {
from("${inputPath}/${samplegen.targetSampleModule()}/src/main/AndroidManifest.xml")
}
// Remove BEGIN_EXCLUDE/END_EXCLUDE blocks from source files
eachFile { file ->
if (file.name.endsWith(".gradle") || file.name.endsWith(".java")) {
// TODO(trevorjohns): Outputs a blank newline for each filtered line. Replace with java.io.FilterReader impl.
boolean outputLines = true;
def removeExcludeBlocksFilter = { line ->
if (line ==~ /\/\/ BEGIN_EXCLUDE/) {
outputLines = false;
} else if (line ==~ /\/\/ END_EXCLUDE/) {
outputLines = true;
} else if (outputLines) {
return line;
}
return ""
}
filter(removeExcludeBlocksFilter)
}
}
}
task emitBrowseable(type:Copy) {
def outputPathRoot = outPath("browseable")
def modules = project.childProjects.keySet()
def hasMultipleModules = modules.size() > 1
println "---------------- modules found in sample: ${modules}"
into outputPathRoot
from("${project.projectDir}/_index.jd")
modules.each { moduleName ->
// For single module samples (default), we emit the module contents
// directly to the root of the browseable sample:
def outputPath = "."
if (hasMultipleModules) {
// For multi module samples, we need an extra directory level
// to separate modules:
outputPath = "${moduleName}"
}
println "\n---------------- processing MODULE ${moduleName} to outputPath ${outputPath}"
def inputPath = "${project.projectDir}/${moduleName}"
def srcDirs = ["main", "common", "template"].collect {input -> "${inputPath}/src/${input}" };
def javaDirs = srcDirs.collect { input -> "${input}/java"}
FileTree javaTree = null;
javaDirs.each { dir ->
FileTree tree = project.fileTree("${dir}")
javaTree = (javaTree == null) ? tree : javaTree.plus(tree)}
Map collapsedPaths = collapsePaths(javaTree, javaDirs)
srcDirs.each { srcPath ->
print "** Copying source ${srcPath}...";
duplicatesStrategy = 'fail'
into("${outputPath}/src") {
def javaPath = "${srcPath}/java";
from(javaPath)
include(["**/*.java", "**/*.xml"])
eachFile { FileCopyDetails fcd ->
if (fcd.file.isFile()) {
def filename = fcd.name;
String collapsed = collapsedPaths.get(fcd.file.path);
fcd.path = "${outputPath}/src/${collapsed}";
} else {fcd.exclude()}
}
println "done"
}
into("${outputPath}/res") {
from("${srcPath}/res")
}
into("${outputPath}/src/rs") {
from("${srcPath}/rs")
}
into("${outputPath}") {from("${srcPath}/AndroidManifest.xml")}
}
}
}
task emitGradleZip(dependsOn: [emitBrowseable, emitGradle], type:Zip) {
def outputPath = "${samplegen.pathToBuild}/out/browseable"
def folderName = "${samplegen.targetSampleName()}"
archiveName = "${samplegen.targetSampleName()}.zip"
def inputPath = outPath("gradle")
from inputPath
into folderName
include "**"
def outDir = project.file(outputPath)
destinationDir = outDir
}

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.path="$USER_HOME$/src/android/developers-dev/developers/samples/android/common/build" external.system.id="GRADLE" type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.gradle" />
<excludeFolder url="file://$MODULE_DIR$/build" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View file

@ -0,0 +1,11 @@
apply plugin: 'groovy'
repositories {
mavenCentral()
}
dependencies {
compile 'org.freemarker:freemarker:2.3.20'
compile gradleApi()
compile localGroovy()
}

View file

@ -0,0 +1,177 @@
/*
* Copyright 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.samples.build
import freemarker.cache.FileTemplateLoader
import freemarker.cache.MultiTemplateLoader
import freemarker.cache.TemplateLoader
import freemarker.template.Configuration
import freemarker.template.DefaultObjectWrapper
import freemarker.template.Template
import org.gradle.api.GradleException
import org.gradle.api.file.FileVisitDetails
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.SourceTask
import org.gradle.api.tasks.TaskAction
class ApplyTemplates extends SourceTask {
/**
* Freemarker context object
*/
def Configuration cfg = new freemarker.template.Configuration()
/**
* The root directory for output files. All output file paths
* are assumed to be relative to this root.
*/
@OutputDirectory
public outputDir = project.projectDir
/**
* Include directory. The templates in this directory will not be
* processed directly, but will be accessible to other templates
* via the <#include> directive.
*/
def include = project.file("$project.projectDir/templates/include")
/**
* List of file extensions that indicate a file to be processed, rather
* than simply copied.
*/
def extensionsToProcess = ['ftl']
/**
* List of file extensions that should be completely ignored by this
* task. File extensions that appear in neither this list nor the list
* specified by {@link #extensionsToProcess} are copied into the destination
* without processing.
*/
def extensionsToIgnore = ['ftli']
/**
* A String -> String closure that transforms a (relative) input path into a
* (relative) output path. This closure is responsible for any alterations to
* the output path, including pathname substitution and extension removal.
*/
Closure<String> filenameTransform
/**
* The hash which will be passed to the freemarker template engine. This hash
* is used by the freemarker script as input data.
* The hash should contain a key named "meta". The template processor will add
* processing data to this key.
*/
def parameters
/**
* The main action for this task. Visits each file in the source directories and
* either processes, copies, or ignores it. The action taken for each file depends
* on the contents of {@link #extensionsToProcess} and {@link #extensionsToIgnore}.
*/
@TaskAction
def applyTemplate() {
// Create a list of Freemarker template loaders based on the
// source tree(s) of this task. The loader list establishes a virtual
// file system for freemarker templates; the template language can
// load files, and each load request will have its path resolved
// against this set of loaders.
println "Gathering template load locations:"
def List loaders = []
source.asFileTrees.each {
src ->
println " ${src.dir}"
loaders.add(0, new FileTemplateLoader(project.file(src.dir)))
}
// Add the include path(s) to the list of loaders.
println "Gathering template include locations:"
include = project.fileTree(include)
include.asFileTrees.each {
inc ->
println " ${inc.dir}"
loaders.add(0, new FileTemplateLoader(project.file(inc.dir)))
}
// Add the loaders to the freemarker config
cfg.setTemplateLoader(new MultiTemplateLoader(loaders.toArray(new TemplateLoader[1])))
// Set the wrapper that will be used to convert the template parameters hash into
// the internal freemarker data model. The default wrapper is capable of handling a
// mix of POJOs/POGOs and XML nodes, so we'll use that.
cfg.setObjectWrapper(new DefaultObjectWrapper())
// This is very much like setting the target SDK level in Android.
cfg.setIncompatibleEnhancements("2.3.20")
// Add an implicit <#include 'common.ftl' to the top of every file.
// TODO: should probably be a parameter instead of hardcoded like this.
cfg.addAutoInclude('common.ftl')
// Visit every file in the source tree(s)
def processTree = source.getAsFileTree()
processTree.visit {
FileVisitDetails input ->
def inputFile = input.getRelativePath().toString()
def outputFile = input.getRelativePath().getFile(project.file(outputDir))
// Get the input and output files, and make sure the output path exists
def renamedOutput = filenameTransform(outputFile.toString())
outputFile = project.file(renamedOutput)
if (input.directory){
// create the output directory. This probably will have already been
// created as part of processing the files *in* the directory, but
// do it here anyway to support empty directories.
outputFile.mkdirs()
} else {
// We may or may not see the directory before we see the files
// in that directory, so create it here
outputFile.parentFile.mkdirs()
// Check the input file extension against the process/ignore list
def extension = "NONE"
def extensionPattern = ~/.*\.(\w*)$/
def extensionMatch = extensionPattern.matcher(inputFile)
if (extensionMatch.matches()) {
extension = extensionMatch[0][1]
}
// If the extension is in the process list, put the input through freemarker
if (extensionsToProcess.contains(extension)){
print '[freemarker] PROCESS: '
println "$inputFile -> $outputFile"
try {
def Template tpl = this.cfg.getTemplate(inputFile)
def FileWriter out = new FileWriter(outputFile)
// Add the output file path to parameters.meta so that the freemarker
// script can access it.
parameters.meta.put("outputFile", "${outputFile}")
tpl.process(parameters, out)
} catch (e) {
println e.message
throw new GradleException("Error processing ${inputFile}: ${e.message}")
}
} else if (!extensionsToIgnore.contains(extension)) {
// if it's not processed and not ignored, then it must be copied.
print '[freemarker] COPY: '
println "$inputFile -> $outputFile"
input.copyTo(outputFile);
}
}
}
}
}

View file

@ -0,0 +1,99 @@
/*
* Copyright 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.samples.build
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.tasks.GradleBuild
/**
* Plugin to expose build rules for sample generation and packaging.
*/
class SampleGenPlugin implements Plugin {
/**
* Creates a new sample generator task based on the supplied sources.
*
* @param name Name of the new task
* @param sources Source tree that this task should process
*/
void createTask(
Project project,
String name,
SampleGenProperties props,
def sources,
def destination) {
project.task ([type:ApplyTemplates], name, {
sources.each { tree ->
source += tree
}
outputDir = destination
include = props.templatesInclude()
filenameTransform = {s -> props.getOutputForInput(s)}
parameters = props.templateParams()
})
}
@Override
void apply(project) {
project.extensions.create("samplegen", SampleGenProperties)
project.samplegen.project = project
SampleGenProperties samplegen = project.samplegen
project.task('create') {
if (project.gradle.startParameter.taskNames.contains('create')) {
samplegen.getCreationProperties()
}
}
project.task('refresh') {
samplegen.getRefreshProperties()
}
project.afterEvaluate({
createTask(project,
'processTemplates',
samplegen,
samplegen.templates(),
samplegen.targetProjectPath)
createTask(project,
'processCommon',
samplegen,
samplegen.common(),
samplegen.targetCommonPath())
project.task([type: GradleBuild], 'bootstrap', {
buildFile = "${samplegen.targetProjectPath}/build.gradle"
dir = samplegen.targetProjectPath
tasks = ["refresh"]
})
project.bootstrap.dependsOn(project.processTemplates)
project.bootstrap.dependsOn(project.processCommon)
project.create.dependsOn(project.bootstrap)
project.refresh.dependsOn(project.processTemplates)
project.refresh.dependsOn(project.processCommon)
// People get nervous when they see a task with no actions, so...
project.create << {println "Project creation finished."}
project.refresh << {println "Project refresh finished."}
})
}
}

View file

@ -0,0 +1,320 @@
/*
* Copyright 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.samples.build
import freemarker.ext.dom.NodeModel
import groovy.transform.Canonical
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.api.file.FileTree
/**
* Gradle extension that holds properties for sample generation.
*
* The sample generator needs a number of properties whose values can be
* inferred by convention from a smaller number of initial properties.
* This class defines fields for the initial properties, and getter
* methods for the inferred properties. It also defines a small number
* of convenience methods for setting up template-generation tasks.
*/
@Canonical
class SampleGenProperties {
/**
* The Gradle project that this extension is being applied to.
*/
Project project
/**
* Directory where the top-level sample project lives
*/
def targetProjectPath
/**
* Relative path to samples/common directory
*/
def pathToSamplesCommon
/**
* Relative path to build directory (platform/developers/build)
*/
def pathToBuild
/**
* Java package name for the root package of this sample.
*/
String targetSamplePackage
/**
*
* @return The path to the sample project (as opposed to the top-level project, which
* what is that even for anyway?)
*/
String targetSamplePath() {
return "${targetProjectPath}/${targetSampleModule()}"
}
/**
*
* @return The path that contains common files -- can be cleaned without harming
* the sample
*/
String targetCommonPath() {
return "${targetSamplePath()}/src/common/java/com/example/android/common"
}
/**
*
* @return The path that contains template files -- can be cleaned without harming
* the sample
*/
String targetTemplatePath() {
return "${targetSamplePath()}/src/template"
}
/**
* The name of this sample (and also of the corresponding .iml file)
*/
String targetSampleName() {
return project.file(targetProjectPath).getName()
}
/**
* The name of the main module in the sample project
*/
String targetSampleModule() {
return "Application"
}
/**
* The path to the template parameters file
*/
String templateXml() {
return "${targetProjectPath}/template-params.xml"
}
/**
* Transforms a package name into a java-style OS dependent path
* @param pkg cccc
* @return The java-style path to the package's code
*/
String packageAsPath(String pkg) {
return pkg.replaceAll(/\./, File.separator)
}
/**
* Transforms a path into a java-style package name
* @param path The java-style path to the package's code
* @return Name of the package to transform
*/
String pathAsPackage(String path) {
return path.replaceAll(File.separator, /\./)
}
/**
* Returns the path to the common/build/templates directory
*/
String templatesRoot() {
return "${targetProjectPath}/${pathToBuild}/templates"
}
/**
* Returns the path to common/src/java
*/
String commonSourceRoot() {
return "${targetProjectPath}/${pathToSamplesCommon}/src/java/com/example/android/common"
}
/**
* Returns the path to the template include directory
*/
String templatesInclude() {
return "${templatesRoot()}/include"
}
/**
* Returns the output file that will be generated for a particular
* input, by replacing generic pathnames with project-specific pathnames
* and dropping the .ftl extension from freemarker files.
*
* @param relativeInputPath Input file as a relative path from the template directory
* @return Relative output file path
*/
String getOutputForInput(String relativeInputPath) {
String outputPath = relativeInputPath
outputPath = outputPath.replaceAll('_PROJECT_', targetSampleName())
outputPath = outputPath.replaceAll('_MODULE_', targetSampleModule())
outputPath = outputPath.replaceAll('_PACKAGE_', packageAsPath(targetSamplePackage))
// This is kind of a hack; IntelliJ picks up any and all subdirectories named .idea, so
// named them ._IDE_ instead. TODO: remove when generating .idea projects is no longer necessary.
outputPath = outputPath.replaceAll('_IDE_', "idea")
outputPath = outputPath.replaceAll(/\.ftl$/, '')
// Any file beginning with a dot won't get picked up, so rename them as necessary here.
outputPath = outputPath.replaceAll('gitignore', '.gitignore')
return outputPath
}
/**
* Returns the tree(s) where the templates to be processed live. The template
* input paths that are passed to
* {@link SampleGenProperties#getOutputForInput(java.lang.String) getOutputForInput}
* are relative to the dir element in each tree.
*/
FileTree[] templates() {
def result = []
def xmlFile = project.file(templateXml())
if (xmlFile.exists()) {
def xml = new XmlSlurper().parse(xmlFile)
xml.template.each { template ->
result.add(project.fileTree(dir: "${templatesRoot()}/${template.@src}"))
}
} else {
result.add(project.fileTree(dir: "${templatesRoot()}/create"))
}
return result;
}
/**
* Path(s) of the common directories to copy over to the sample project.
*/
FileTree[] common() {
def result = []
def xmlFile = project.file(templateXml())
if (xmlFile.exists()) {
def xml = new XmlSlurper().parse(xmlFile)
xml.common.each { common ->
println "Adding common/${common.@src} from ${commonSourceRoot()}"
result.add(project.fileTree (
dir: "${commonSourceRoot()}",
include: "${common.@src}/**/*"
))
}
}
return result
}
/**
* Returns the hash to supply to the freemarker template processor.
* This is loaded from the file specified by {@link SampleGenProperties#templateXml()}
* if such a file exists, or synthesized with some default parameters if it does not.
* In addition, some data about the current project is added to the "meta" key of the
* hash.
*
* @return The hash to supply to freemarker
*/
Map templateParams() {
Map result = new HashMap();
def xmlFile = project.file(templateXml())
if (xmlFile.exists()) {
// Parse the xml into Freemarker's DOM structure
def params = freemarker.ext.dom.NodeModel.parse(xmlFile)
// Move to the <sample> node and stuff that in our map
def sampleNode = (NodeModel)params.exec(['/sample'])
result.put("sample", sampleNode)
} else {
// Fake data for use on creation
result.put("sample", [
name:targetSampleName(),
package:targetSamplePackage,
minSdk:4
])
}
// Extra data that some templates find useful
result.put("meta", [
root: targetProjectPath,
module: targetSampleModule(),
common: pathToSamplesCommon,
build: pathToBuild,
])
return result
}
/**
* Generate default values for properties that can be inferred from an existing
* generated project, unless those properties have already been
* explicitly specified.
*/
void getRefreshProperties() {
if (!this.targetProjectPath) {
this.targetProjectPath = project.projectDir
}
def xmlFile = project.file(templateXml())
if (xmlFile.exists()) {
println "Template XML: $xmlFile"
def xml = new XmlSlurper().parse(xmlFile)
this.targetSamplePackage = xml.package.toString()
println "Target Package: $targetSamplePackage"
}
}
/**
* Generate default values for creation properties, unless those properties
* have already been explicitly specified. This method will attempt to get
* these properties interactively from the user if necessary.
*/
void getCreationProperties() {
def calledFrom = project.hasProperty('calledFrom') ? new File(project.calledFrom)
: project.projectDir
calledFrom = calledFrom.getCanonicalPath()
println('\n\n\nReady to create project...')
if (project.hasProperty('pathToSamplesCommon')) {
this.pathToSamplesCommon = project.pathToSamplesCommon
} else {
throw new GradleException (
'create task requires project property pathToSamplesCommon')
}
if (project.hasProperty('pathToBuild')) {
this.pathToBuild = project.pathToBuild
} else {
throw new GradleException ('create task requires project property pathToBuild')
}
if (!this.targetProjectPath) {
if (project.hasProperty('out')) {
this.targetProjectPath = project.out
} else {
this.targetProjectPath = System.console().readLine(
"\noutput directory [$calledFrom]:")
if (this.targetProjectPath.length() <= 0) {
this.targetProjectPath = calledFrom
}
}
}
if (!this.targetSamplePackage) {
def defaultPackage = "com.example.android." +
this.targetSampleName().toLowerCase()
this.targetSamplePackage = System.console().readLine(
"\nsample package name[$defaultPackage]:")
if (this.targetSamplePackage.length() <= 0) {
this.targetSamplePackage = defaultPackage
}
}
}
}

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/groovy" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View file

@ -0,0 +1,358 @@
#!/usr/bin/env bash
##############################################################################
##
## GitHub Upload+Update Script (V2, combined) for DevPlat Samples
##
##############################################################################
update=true
upload=true
deleteTemp=true
useAllSamples=true
allSamples=()
token=
## Generates a random 32 character alphaneumeric string to use as a post script
## for the temporary code folder (folder will be deleted at end)
folderPS=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)
#utility function to print to stderr
echoerr() { echo "$@" 1>&2; }
display_usage() {
echo -e "\e[90mUsage:
-t | --token [github_auth_token]
Input an auth token to access the googlesamples GitHub org
(if this is not present, you will be prompted for one later)
-s | --samples [sample1 sample2 sample3 ... sampleN]
If you don't want to check the entire samples folder,
you can specify which samples to use with this option.
--upload-only
Only uploads new samples - samples with existing
repos will be ignored
--update-only
Only updates samples with existing repos - new
samples will be ignored
--keep-temp-files
Will not delete the temporary directory used to pull/push
to Github. (normally deleted upon exit) Preserves logs.
This script can be run with no options - it will check the entire
./prebuilts/gradle folder and prompt for an auth token when needed.\e[0m\n"
}
##############################################################################
## Make sure we delete the temporary folder (if it gets created) before exiting
finish() {
if $deleteTemp; then
if [ -d "../github-temp$folderPS" ]; then
cd ..
rm -rf ./github-temp$folderPS
elif [ -d "github-temp$folderPS" ]; then
rm -rf ./github-temp$folderPS
fi
fi
}
# this ensures finish() will always be called no matter how the script ends
trap finish EXIT
##############################################################################
## Process input parameters. (see above for usage)
## How this works:
## $# is the number of parameters passed in
## $1 is the first parameter, $2 the second, and so on. (space delimited)
## shift basically left shifts the params array - $1 goes away, $2 becomes $1, etc
## Thus, this while loop iterates through all command line parameters
while [[ $# > 0 ]]; do
case "$1" in
-t|--token)
if [[ $2 != -* ]] && [[ $2 ]]; then
token="$2"; shift
else
echoerr -e "Option $1 requires an argument. Cancelling script.\nUse --help to display usage."
exit 1
fi;;
--update-only) upload=false;;
--upload-only) update=false;;
--keep-temp-files) deleteTemp=false;;
-s|--samples)
useAllSamples=false
while [[ $2 != -* ]] && [[ $2 ]]; do
#if true; then ##for testing
if [ -d "./prebuilts/gradle/$2" ]; then
allSamples+=("$2")
shift
else
echoerr -e "Sample \"$2\" does not exist in ./prebuilts/gradle. Cancelling script.\n"
exit 1
fi
done;;
-h|--help)
display_usage
exit 1;;
*)
echoerr -e "Unknown Option: $1\nUse --help to display usage."
exit 1;;
esac
shift
done #ends options while loop
if ! $upload && ! $update; then
echoerr -e "Do not use both --update-only and --upload-only, no samples will be processed.
If you want to do both updates and uploads, no flags are needed.
Use --help to display usage."
exit 1
fi
##############################################################################
## Get all folders in prebuilts and stick 'em in an array
if $useAllSamples; then
allSamples=($(ls ./prebuilts/gradle))
fi
# [@] returns all items in an array, ${#...} counts them
numSamples=${#allSamples[@]}
echo "Running script for $numSamples samples"
##############################################################################
## Iterate through all the samples and see if there's
## a repo for them on GitHub already - save results so we only do it once
toUpdate=()
toUpload=()
problemSamples=()
curSample=0
echo -ne "Checking for existence of repos... ($curSample/$numSamples)\r"
for i in ${allSamples[@]};
do
#echo "$i"
URL=https://github.com/googlesamples/android-$i
result=$(curl -o /dev/null --silent --head --write-out '%{http_code}' "$URL")
#echo "$result $URL"
if [ "$result" -eq "404" ]; then
toUpload+=("$i")
elif [ "$result" -eq "200" ]; then
toUpdate+=("$i")
else
problemSamples+=("$i")
fi
curSample=$(($curSample+1))
echo -ne "Checking for existence of repos... ($curSample/$numSamples)\r"
done #close for loop for existence check
echo ""
##############################################################################
## For every sample that has a repo already, clone it and diff it against
## the sample code in our git to see if it needs updating.
if $update; then
needsUpdate=()
curSample=0
numUpdates=${#toUpdate[@]}
##make temporary dir to pull code into - will be deleted upon exit.
mkdir github-temp$folderPS
cd github-temp$folderPS
echo -ne "Checking for out-of-date repos... ($curSample/$numUpdates)\r"
for i in ${toUpdate[@]};
do
URL=https://github.com/googlesamples/android-$i
git clone $URL.git &> /dev/null
if [ -d "android-$i" ]; then
diffResult=$(diff -r --exclude '*.git' ../prebuilts/gradle/$i/ ./android-$i/)
#for testing (will show diff in every repo)
#diffResult=$(diff -r ../prebuilts/gradle/$i/ ./android-$i/)`
#echo $diffResult
if [ -n "$diffResult" ]; then
needsUpdate+=("$i")
fi
else
echoerr "Something went wrong when cloning $i - result directory does not exist.
Leaving temp files in place for further examination."
deleteTemp=false;
fi
curSample=$(($curSample+1))
echo -ne "Checking for out-of-date repos... ($curSample/$numUpdates)\r"
done #end of for loop when checking which repos actually need updating
echo ""
fi
echo ""
##############################################################################
## Display the detected changes to be made and get user confirmation
if $upload; then
if [ ${#toUpload[@]} -ne 0 ]; then
echo -e "\n\e[1mNew samples that will be uploaded:\e[0m"
for i in ${toUpload[@]}; do
echo -e "\e[32m$i\e[0m"
done
else
upload=false
echo "Nothing new to upload."
fi
else
echo "No uploads - check skipped on user request"
fi
if $update; then
if [ ${#needsUpdate[@]} -ne 0 ]; then
echo -e "\n\e[1mSamples that will be updated:\e[0m"
for i in ${needsUpdate[@]}; do
echo -e "\e[34m$i\e[0m"
done
else
update=false
echo "Nothing to update."
fi
else
echo "No updates - check skipped on user request"
fi
if [ ${#problemSamples[@]} -ne 0 ]; then
echoerr "
These repos returned something other than a 404 or 200 result code:"
for i in ${problemSamples[@]};
do
echoerr "$i"
done
fi
if ! $upload && ! $update; then
echo -e "\e[1mLooks like everything's up-to-date.\e[0m\n"
else
read -p "
Do you want to continue? [y/n]: " -n 1 -r
echo
# if they type anything but an upper or lower case y, don't proceed.
if [[ $REPLY =~ ^[Yy]$ ]]
then
#echo "Commencing Github updates"
##############################################################################
## If the user hasn't supplied a token via parameter, ask now
if ! [ -n "$token" ]
then
read -p "
Input a valid googlesamples GitHub access token to continue: " -r
token=$REPLY
fi
##############################################################################
## Test that token
tokenTest=$(curl -o /dev/null --silent \
-H "Authorization: token $token" \
--write-out '%{http_code}' "https://api.github.com/orgs/googlesamples/repos")
if [ "$tokenTest" -eq "200" ]; then
##############################################################################
## If there's something to update, do the updates
if [ ${#needsUpdate[@]} -ne 0 ] && $update; then
for i in ${needsUpdate[@]}; do
echo -e "\nUpdating $i"
if [ -d "android-$i" ]; then
rsync -az --delete --exclude '*.git' ../prebuilts/gradle/$i/ ./android-$i/
cd ./android-$i/
git config user.name "google-automerger"
git config user.email automerger@google.com
git add .
git status
git commit -m "Auto-update"
git remote set-url origin "https://$token@github.com/googlesamples/android-$i.git"
git push origin master
#overwrite remote url to not contain auth token
git remote set-url origin "http://github.com/googlesamples/android-$i.git"
cd ..
else
echoerr "Something went wrong when cloning $i - result directory does not exist.
Leaving temp files in place for further examination."
deleteTemp=false;
fi
done
fi
#moves out of the temp folder, if we're in it.
if [ -d "../github-temp$folderPS" ]; then
cd ..
fi
##############################################################################
## If there's something new to upload, do the uploads
if [ ${#toUpload[@]} -ne 0 ] && $upload; then
for i in ${toUpload[@]}; do
echo -e "\nUploading $i"
repoName="googlesamples/android-$i"
CREATE="curl -H 'Authorization: token '$TOKEN \
-d '{\"name\":\"android-'$i'\", \"team_id\":889859}' \
https://api.github.com/orgs/googlesamples/repos"
eval $CREATE
#add secondary team permissions (robots)
ADDTEAM="curl -X PUT \
-H 'Authorization: token '$TOKEN \
-H 'Content-Length: 0' \
https://api.github.com/teams/889856/repos/$repoName"
eval $ADDTEAM
URL="https://$token@github.com/$repoName"
cd $i
git init
#overrides .gitconfig just for this project - does not alter your global settings.
git config user.name "google-automerger"
git config user.email automerger@google.com
git add .
git commit -m "Initial Commit"
git remote add origin $URL
git push origin master
cd ..
done
fi
else
echoerr "That token doesn't work. A test returned the code: $tokenTest"
fi
else
echo "User cancelled Github update."
fi
fi #end of "is there something to do?" if statement

Binary file not shown.

View file

@ -0,0 +1,6 @@
#Tue May 17 22:12:39 PDT 2016
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip

164
android/developers/build/gradlew vendored Executable file
View file

@ -0,0 +1,164 @@
#!/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
# For Cygwin, ensure paths are in UNIX format before anything is touched.
if $cygwin ; then
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
fi
# 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\"`/" >&-
APP_HOME="`pwd -P`"
cd "$SAVED" >&-
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"`
# 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 "$@"

90
android/developers/build/gradlew.bat vendored Normal file
View file

@ -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

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,56 @@
# How to become a contributor and submit your own code
## Contributor License Agreements
We'd love to accept your sample apps and patches! Before we can take them, we
have to jump a couple of legal hurdles.
Please fill out either the individual or corporate Contributor License Agreement
(CLA).
* If you are an individual writing original source code and you're sure you
own the intellectual property, then you'll need to sign an [individual CLA]
(http://code.google.com/legal/individual-cla-v1.0.html).
* If you work for a company that wants to allow you to contribute your work,
then you'll need to sign a [corporate CLA]
(http://code.google.com/legal/corporate-cla-v1.0.html).
Follow either of the two links above to access the appropriate CLA and
instructions for how to sign and return it. Once we receive it, we'll be able to
accept your pull requests.
## Contributing a Patch
1. Sign a Contributor License Agreement, if you have not yet done so (see
details above).
1. Create your change to the repo in question.
* Fork the desired repo, develop and test your code changes.
* Ensure that your code is clear and comprehensible.
* Ensure that your code has an appropriate set of unit tests which all pass.
1. Submit a pull request.
1. The repo owner will review your request. If it is approved, the change will
be merged. If it needs additional work, the repo owner will respond with
useful comments.
## Contributing a New Sample App
1. Sign a Contributor License Agreement, if you have not yet done so (see
details above).
1. Create your own repo for your app following this naming convention:
* mirror-{app-name}-{language or plaform}
* apps: quickstart, photohunt-server, photohunt-client
* example: mirror-quickstart-android
* For multi-language apps, concatenate the primary languages like this:
mirror-photohunt-server-java-python.
1. Create your sample app in this repo.
* Be sure to clone the README.md, CONTRIBUTING.md and LICENSE files from the
googlesamples repo.
* Ensure that your code is clear and comprehensible.
* Ensure that your code has an appropriate set of unit tests which all pass.
* Instructional value is the top priority when evaluating new app proposals for
this collection of repos.
1. Submit a request to fork your repo in googlesamples organization.
1. The repo owner will review your request. If it is approved, the sample will
be merged. If it needs additional work, the repo owner will respond with
useful comments.

View file

@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, "control" means (i) the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising
permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
"Object" form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
2. Grant of Copyright License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
3. Grant of Patent License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
4. Redistribution.
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
You must give any other recipients of the Work or Derivative Works a copy of
this License; and
You must cause any modified files to carry prominent notices stating that You
changed the files; and
You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
If the Work includes a "NOTICE" text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
5. Submission of Contributions.
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
6. Trademarks.
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
7. Disclaimer of Warranty.
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
8. Limitation of Liability.
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability.
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets "[]" replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same "printed page" as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -0,0 +1,25 @@
# AndroidTV Leanback Support Library sample for videos
The Leanback API Demo/Video By Googles app is designed to run on an Android TV device and demonstrates how to use the Leanback Support library
in order to comply with the UX guidelines of Android TV.
## Dependencies
* Android SDK v7 appcompat library
* Android SDK v17 leanback support library
* Android SDK v7 recyclerview library
## Setup Instructions
* Compile and deploy to your Android TV device.
## References and How to report bugs
* [Android TV Developer Documentation](http://developer.android.com/tv)
## How to make contributions?
Please read and follow the steps in the CONTRIBUTING.md
## License
See LICENSE
## Google+
Android TV Community Page on Google+ [https://g.co/androidtvdev](https://g.co/androidtvdev)
## Change List

View file

@ -0,0 +1,28 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 22
buildToolsVersion '22.0.0'
defaultConfig {
applicationId "com.example.android.tvleanback"
minSdkVersion 21
targetSdkVersion 22
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:recyclerview-v7:22.0.0'
compile 'com.android.support:leanback-v17:22.0.0'
compile 'com.android.support:appcompat-v7:22.0.0'
compile 'com.github.bumptech.glide:glide:3.4.+'
compile 'com.android.support:support-v4:22.0.0'
}

View file

@ -0,0 +1,17 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /usr/local/google/home/cartland/android-sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

View file

@ -0,0 +1,114 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2014 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.tvleanback"
android:versionCode="1"
android:versionName="1.1" >
<uses-sdk
android:minSdkVersion="19"
android:targetSdkVersion="21" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-feature
android:name="android.hardware.microphone"
android:required="false" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-feature android:name="android.software.leanback"
android:required="true" />
<application
android:allowBackup="false"
android:icon="@drawable/videos_by_google_banner"
android:label="@string/app_name"
android:logo="@drawable/videos_by_google_banner"
android:theme="@style/Theme.Example.Leanback" >
<activity
android:name=".ui.MainActivity"
android:icon="@drawable/videos_by_google_banner"
android:label="@string/app_name"
android:logo="@drawable/videos_by_google_banner"
android:screenOrientation="landscape" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ui.MovieDetailsActivity"
android:exported="true">
<!-- Receives the search request. -->
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
<!-- No category needed, because the Intent will specify this class component-->
</intent-filter>
<!-- Points to searchable meta data. -->
<meta-data android:name="android.app.searchable"
android:resource="@xml/searchable" />
</activity>
<activity android:name=".ui.PlaybackOverlayActivity"
android:exported="true" />
<activity
android:name=".ui.VerticalGridActivity"
android:exported="true"
android:parentActivityName=".ui.MainActivity" />
<activity
android:name=".ui.SearchActivity"
android:exported="true" />
<activity android:name=".ui.BrowseErrorActivity"
android:exported="true" />
<!-- Provides search suggestions for keywords against video meta data. -->
<provider android:name=".data.VideoContentProvider"
android:authorities="com.example.android.tvleanback"
android:exported="true" />
<provider
android:name=".recommendation.RecommendationBuilder$RecommendationBackgroundContentProvider"
android:authorities="com.example.android.tvleanback.recommendation"
android:exported="true" />
<receiver
android:name=".recommendation.BootupActivity"
android:enabled="true"
android:exported="false" >
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<service
android:name=".recommendation.UpdateRecommendationsService"
android:enabled="true" />
</application>
</manifest>

View file

@ -0,0 +1,52 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.example.android.leanback;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
/*
* This class extends BroadCastReceiver and publishes recommendations on bootup
*/
public class BootupActivity extends BroadcastReceiver {
private static final String TAG = "BootupActivity";
private static final long INITIAL_DELAY = 5000;
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "BootupActivity initiated");
if (intent.getAction().endsWith(Intent.ACTION_BOOT_COMPLETED)) {
scheduleRecommendationUpdate(context);
}
}
private void scheduleRecommendationUpdate(Context context) {
Log.d(TAG, "Scheduling recommendations update");
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Intent recommendationIntent = new Intent(context, UpdateRecommendationsService.class);
PendingIntent alarmIntent = PendingIntent.getService(context, 0, recommendationIntent, 0);
alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
INITIAL_DELAY,
AlarmManager.INTERVAL_HALF_HOUR,
alarmIntent);
}
}

View file

@ -0,0 +1,136 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.example.android.leanback;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.support.v17.leanback.widget.ImageCardView;
import android.support.v17.leanback.widget.Presenter;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;
import java.net.URI;
/*
* A CardPresenter is used to generate Views and bind Objects to them on demand.
* It contains an Image CardView
*/
public class CardPresenter extends Presenter {
private static final String TAG = "CardPresenter";
private static Context mContext;
private static int CARD_WIDTH = 313;
private static int CARD_HEIGHT = 176;
static class ViewHolder extends Presenter.ViewHolder {
private Movie mMovie;
private ImageCardView mCardView;
private Drawable mDefaultCardImage;
private PicassoImageCardViewTarget mImageCardViewTarget;
public ViewHolder(View view) {
super(view);
mCardView = (ImageCardView) view;
mImageCardViewTarget = new PicassoImageCardViewTarget(mCardView);
mDefaultCardImage = mContext.getResources().getDrawable(R.drawable.movie);
}
public void setMovie(Movie m) {
mMovie = m;
}
public Movie getMovie() {
return mMovie;
}
public ImageCardView getCardView() {
return mCardView;
}
protected void updateCardViewImage(URI uri) {
Picasso.with(mContext)
.load(uri.toString())
.resize(Utils.dpToPx(CARD_WIDTH, mContext), Utils.dpToPx(CARD_HEIGHT, mContext))
.error(mDefaultCardImage)
.into(mImageCardViewTarget);
}
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent) {
Log.d(TAG, "onCreateViewHolder");
mContext = parent.getContext();
ImageCardView cardView = new ImageCardView(mContext);
cardView.setFocusable(true);
cardView.setFocusableInTouchMode(true);
cardView.setBackgroundColor(mContext.getResources().getColor(R.color.fastlane_background));
return new ViewHolder(cardView);
}
@Override
public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) {
Movie movie = (Movie) item;
((ViewHolder) viewHolder).setMovie(movie);
Log.d(TAG, "onBindViewHolder");
if (movie.getCardImageUrl() != null) {
((ViewHolder) viewHolder).mCardView.setTitleText(movie.getTitle());
((ViewHolder) viewHolder).mCardView.setContentText(movie.getStudio());
((ViewHolder) viewHolder).mCardView.setMainImageDimensions(CARD_WIDTH, CARD_HEIGHT);
((ViewHolder) viewHolder).updateCardViewImage(movie.getCardImageURI());
}
}
@Override
public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) {
Log.d(TAG, "onUnbindViewHolder");
}
@Override
public void onViewAttachedToWindow(Presenter.ViewHolder viewHolder) {
// TO DO
}
public static class PicassoImageCardViewTarget implements Target {
private ImageCardView mImageCardView;
public PicassoImageCardViewTarget(ImageCardView imageCardView) {
mImageCardView = imageCardView;
}
@Override
public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom loadedFrom) {
Drawable bitmapDrawable = new BitmapDrawable(mContext.getResources(), bitmap);
mImageCardView.setMainImage(bitmapDrawable);
}
@Override
public void onBitmapFailed(Drawable drawable) {
mImageCardView.setMainImage(drawable);
}
@Override
public void onPrepareLoad(Drawable drawable) {
// Do nothing, default_background manager has its own transitions
}
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.example.android.leanback;
import android.app.Activity;
import android.os.Bundle;
/*
* A wrapper class for details activity
*/
public class DetailsActivity extends Activity
{
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.details);
}
}

View file

@ -0,0 +1,31 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.example.android.leanback;
import android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter;
public class DetailsDescriptionPresenter extends AbstractDetailsDescriptionPresenter {
@Override
protected void onBindDescription(ViewHolder viewHolder, Object item) {
Movie movie = (Movie) item;
if (movie != null) {
viewHolder.getTitle().setText(movie.getTitle());
viewHolder.getSubtitle().setText(movie.getStudio());
viewHolder.getBody().setText(movie.getDescription());
}
}
}

View file

@ -0,0 +1,193 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.example.android.leanback;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v17.leanback.app.BackgroundManager;
import android.support.v17.leanback.app.DetailsFragment;
import android.support.v17.leanback.widget.Action;
import android.support.v17.leanback.widget.ArrayObjectAdapter;
import android.support.v17.leanback.widget.ClassPresenterSelector;
import android.support.v17.leanback.widget.DetailsOverviewRow;
import android.support.v17.leanback.widget.DetailsOverviewRowPresenter;
import android.support.v17.leanback.widget.HeaderItem;
import android.support.v17.leanback.widget.ListRow;
import android.support.v17.leanback.widget.ListRowPresenter;
import android.support.v17.leanback.widget.OnActionClickedListener;
import android.support.v17.leanback.widget.OnItemClickedListener;
import android.support.v17.leanback.widget.Row;
import android.util.DisplayMetrics;
import android.util.Log;
import android.widget.Toast;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;
import java.io.IOException;
import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/*
* LeanbackDetailsFragment extends DetailsFragment, a Wrapper fragment for leanback details screens.
* It shows a detailed view of video and its meta plus related videos.
*/
public class LeanbackDetailsFragment extends DetailsFragment {
private static final String TAG = "DetailsFragment";
private static final int ACTION_WATCH_TRAILER = 1;
private static final int ACTION_RENT = 2;
private static final int ACTION_BUY = 3;
private static final int DETAIL_THUMB_WIDTH = 274;
private static final int DETAIL_THUMB_HEIGHT = 274;
private Movie selectedMovie;
private Drawable mDefaultBackground;
private Target mBackgroundTarget;
private DisplayMetrics mMetrics;
@Override
public void onCreate(Bundle savedInstanceState) {
Log.i(TAG, "onCreate DetailsFragment");
super.onCreate(savedInstanceState);
BackgroundManager backgroundManager = BackgroundManager.getInstance(getActivity());
backgroundManager.attach(getActivity().getWindow());
mBackgroundTarget = new PicassoBackgroundManagerTarget(backgroundManager);
mDefaultBackground = getResources().getDrawable(R.drawable.default_background);
mMetrics = new DisplayMetrics();
getActivity().getWindowManager().getDefaultDisplay().getMetrics(mMetrics);
selectedMovie = (Movie) getActivity().getIntent().getSerializableExtra("Movie");
Log.d(TAG, "DetailsActivity movie: " + selectedMovie.toString());
new DetailRowBuilderTask().execute(selectedMovie);
setOnItemClickedListener(getDefaultItemClickedListener());
updateBackground(selectedMovie.getBackgroundImageURI());
}
private class DetailRowBuilderTask extends AsyncTask<Movie, Integer, DetailsOverviewRow> {
@Override
protected DetailsOverviewRow doInBackground(Movie... movies) {
selectedMovie = movies[0];
Log.d(TAG, "doInBackground: " + selectedMovie.toString());
DetailsOverviewRow row = new DetailsOverviewRow(selectedMovie);
try {
Bitmap poster = Picasso.with(getActivity())
.load(selectedMovie.getCardImageUrl())
.resize(Utils.dpToPx(DETAIL_THUMB_WIDTH, getActivity()
.getApplicationContext()),
Utils.dpToPx(DETAIL_THUMB_HEIGHT, getActivity()
.getApplicationContext()))
.centerCrop()
.get();
row.setImageBitmap(getActivity(), poster);
} catch (IOException e) {
}
row.addAction(new Action(ACTION_WATCH_TRAILER, getResources().getString(
R.string.watch_trailer_1), getResources().getString(R.string.watch_trailer_2)));
row.addAction(new Action(ACTION_RENT, getResources().getString(R.string.rent_1),
getResources().getString(R.string.rent_2)));
row.addAction(new Action(ACTION_BUY, getResources().getString(R.string.buy_1),
getResources().getString(R.string.buy_2)));
return row;
}
@Override
protected void onPostExecute(DetailsOverviewRow detailRow) {
ClassPresenterSelector ps = new ClassPresenterSelector();
DetailsOverviewRowPresenter dorPresenter =
new DetailsOverviewRowPresenter(new DetailsDescriptionPresenter());
// set detail background and style
dorPresenter.setBackgroundColor(getResources().getColor(R.color.detail_background));
dorPresenter.setStyleLarge(true);
dorPresenter.setOnActionClickedListener(new OnActionClickedListener() {
@Override
public void onActionClicked(Action action) {
if (action.getId() == ACTION_WATCH_TRAILER) {
Intent intent = new Intent(getActivity(), PlayerActivity.class);
intent.putExtra(getResources().getString(R.string.movie), selectedMovie);
intent.putExtra(getResources().getString(R.string.should_start), true);
startActivity(intent);
}
else {
Toast.makeText(getActivity(), action.toString(), Toast.LENGTH_SHORT).show();
}
}
});
ps.addClassPresenter(DetailsOverviewRow.class, dorPresenter);
ps.addClassPresenter(ListRow.class,
new ListRowPresenter());
ArrayObjectAdapter adapter = new ArrayObjectAdapter(ps);
adapter.add(detailRow);
String subcategories[] = {
getString(R.string.related_movies)
};
HashMap<String, List<Movie>> movies = VideoProvider.getMovieList();
ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(new CardPresenter());
for (Map.Entry<String, List<Movie>> entry : movies.entrySet())
{
if (selectedMovie.getCategory().indexOf(entry.getKey()) >= 0) {
List<Movie> list = entry.getValue();
for (int j = 0; j < list.size(); j++) {
listRowAdapter.add(list.get(j));
}
}
}
HeaderItem header = new HeaderItem(0, subcategories[0], null);
adapter.add(new ListRow(header, listRowAdapter));
setAdapter(adapter);
}
}
protected OnItemClickedListener getDefaultItemClickedListener() {
return new OnItemClickedListener() {
@Override
public void onItemClicked(Object item, Row row) {
if (item instanceof Movie) {
Movie movie = (Movie) item;
Intent intent = new Intent(getActivity(), DetailsActivity.class);
intent.putExtra(getResources().getString(R.string.movie), movie);
startActivity(intent);
}
}
};
}
protected void updateBackground(URI uri) {
Picasso.with(getActivity())
.load(uri.toString())
.resize(mMetrics.widthPixels, mMetrics.heightPixels)
.error(mDefaultBackground)
.into(mBackgroundTarget);
}
}

View file

@ -0,0 +1,33 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.example.android.leanback;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
/*
* A wrapper class for main view of the app
*/
public class MainActivity extends Activity {
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
}
}

View file

@ -0,0 +1,291 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.example.android.leanback;
import android.app.LoaderManager;
import android.content.Intent;
import android.content.Loader;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
import android.support.v17.leanback.app.BackgroundManager;
import android.support.v17.leanback.app.BrowseFragment;
import android.support.v17.leanback.widget.ArrayObjectAdapter;
import android.support.v17.leanback.widget.HeaderItem;
import android.support.v17.leanback.widget.ListRow;
import android.support.v17.leanback.widget.ListRowPresenter;
import android.support.v17.leanback.widget.OnItemClickedListener;
import android.support.v17.leanback.widget.OnItemSelectedListener;
import android.support.v17.leanback.widget.Presenter;
import android.support.v17.leanback.widget.Row;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;
import java.net.URI;
import java.util.*;
/*
* Main class to show BrowseFragment with header and rows of videos
*/
public class MainFragment extends BrowseFragment implements
LoaderManager.LoaderCallbacks<HashMap<String, List<Movie>>> {
private static final String TAG = "MainFragment";
private static int BACKGROUND_UPDATE_DELAY = 300;
private static int GRID_ITEM_WIDTH = 200;
private static int GRID_ITEM_HEIGHT = 200;
private ArrayObjectAdapter mRowsAdapter;
private Drawable mDefaultBackground;
private Target mBackgroundTarget;
private DisplayMetrics mMetrics;
private Timer mBackgroundTimer;
private final Handler mHandler = new Handler();
private URI mBackgroundURI;
private static String mVideosUrl;
@Override
public void onActivityCreated(Bundle savedInstanceState) {
Log.i(TAG, "onCreate");
super.onActivityCreated(savedInstanceState);
loadVideoData();
prepareBackgroundManager();
setupUIElements();
setupEventListeners();
}
private void prepareBackgroundManager() {
BackgroundManager backgroundManager = BackgroundManager.getInstance(getActivity());
backgroundManager.attach(getActivity().getWindow());
mBackgroundTarget = new PicassoBackgroundManagerTarget(backgroundManager);
mDefaultBackground = getResources().getDrawable(R.drawable.default_background);
mMetrics = new DisplayMetrics();
getActivity().getWindowManager().getDefaultDisplay().getMetrics(mMetrics);
}
private void setupUIElements() {
// setBadgeDrawable(getActivity().getResources().getDrawable(R.drawable.videos_by_google_banner));
setTitle(getString(R.string.browse_title)); // Badge, when set, takes precedent over title
setHeadersState(HEADERS_ENABLED);
setHeadersTransitionOnBackEnabled(true);
// set fastLane (or headers) background color
setBrandColor(getResources().getColor(R.color.fastlane_background));
// set search icon color
setSearchAffordanceColor(getResources().getColor(R.color.search_opaque));
}
private void loadVideoData() {
VideoProvider.setContext(getActivity());
mVideosUrl = getActivity().getResources().getString(R.string.catalog_url);
getLoaderManager().initLoader(0, null, this);
}
private void setupEventListeners() {
setOnSearchClickedListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(getActivity(), SearchActivity.class);
startActivity(intent);
}
});
setOnItemSelectedListener(getDefaultItemSelectedListener());
setOnItemClickedListener(getDefaultItemClickedListener());
}
/*
* (non-Javadoc)
* @see android.support.v4.app.LoaderManager.LoaderCallbacks#onCreateLoader(int,
* android.os.Bundle)
*/
@Override
public Loader<HashMap<String, List<Movie>>> onCreateLoader(int arg0, Bundle arg1) {
Log.d(TAG, "VideoItemLoader created ");
return new VideoItemLoader(getActivity(), mVideosUrl);
}
/*
* (non-Javadoc)
* @see android.support.v4.app.LoaderManager.LoaderCallbacks#onLoadFinished(android
* .support.v4.content.Loader, java.lang.Object)
*/
@Override
public void onLoadFinished(Loader<HashMap<String, List<Movie>>> arg0,
HashMap<String, List<Movie>> data) {
mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
CardPresenter cardPresenter = new CardPresenter();
int i = 0;
for (Map.Entry<String, List<Movie>> entry : data.entrySet())
{
ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(cardPresenter);
List<Movie> list = entry.getValue();
for (int j = 0; j < list.size(); j++) {
listRowAdapter.add(list.get(j));
}
HeaderItem header = new HeaderItem(i, entry.getKey(), null);
i++;
mRowsAdapter.add(new ListRow(header, listRowAdapter));
}
HeaderItem gridHeader = new HeaderItem(i, getResources().getString(R.string.preferences),
null);
GridItemPresenter gridPresenter = new GridItemPresenter();
ArrayObjectAdapter gridRowAdapter = new ArrayObjectAdapter(gridPresenter);
gridRowAdapter.add(getResources().getString(R.string.grid_view));
gridRowAdapter.add(getResources().getString(R.string.send_feeback));
gridRowAdapter.add(getResources().getString(R.string.personal_settings));
mRowsAdapter.add(new ListRow(gridHeader, gridRowAdapter));
setAdapter(mRowsAdapter);
updateRecommendations();
}
@Override
public void onLoaderReset(Loader<HashMap<String, List<Movie>>> arg0) {
mRowsAdapter.clear();
}
protected OnItemSelectedListener getDefaultItemSelectedListener() {
return new OnItemSelectedListener() {
@Override
public void onItemSelected(Object item, Row row) {
if (item instanceof Movie) {
mBackgroundURI = ((Movie) item).getBackgroundImageURI();
startBackgroundTimer();
}
}
};
}
protected OnItemClickedListener getDefaultItemClickedListener() {
return new OnItemClickedListener() {
@Override
public void onItemClicked(Object item, Row row) {
if (item instanceof Movie) {
Movie movie = (Movie) item;
Log.d(TAG, "Item: " + item.toString());
Intent intent = new Intent(getActivity(), DetailsActivity.class);
intent.putExtra(getString(R.string.movie), movie);
startActivity(intent);
}
else if (item instanceof String) {
if (((String) item).indexOf(getResources().getString(R.string.grid_view)) >= 0) {
Intent intent = new Intent(getActivity(), VerticalGridActivity.class);
startActivity(intent);
}
else {
Toast.makeText(getActivity(), ((String) item), Toast.LENGTH_SHORT)
.show();
}
}
}
};
}
protected void setDefaultBackground(Drawable background) {
mDefaultBackground = background;
}
protected void setDefaultBackground(int resourceId) {
mDefaultBackground = getResources().getDrawable(resourceId);
}
protected void updateBackground(URI uri) {
Picasso.with(getActivity())
.load(uri.toString())
.resize(mMetrics.widthPixels, mMetrics.heightPixels)
.centerCrop()
.error(mDefaultBackground)
.into(mBackgroundTarget);
}
protected void updateBackground(Drawable drawable) {
BackgroundManager.getInstance(getActivity()).setDrawable(drawable);
}
protected void clearBackground() {
BackgroundManager.getInstance(getActivity()).setDrawable(mDefaultBackground);
}
private void startBackgroundTimer() {
if (null != mBackgroundTimer) {
mBackgroundTimer.cancel();
}
mBackgroundTimer = new Timer();
mBackgroundTimer.schedule(new UpdateBackgroundTask(), BACKGROUND_UPDATE_DELAY);
}
private class UpdateBackgroundTask extends TimerTask {
@Override
public void run() {
mHandler.post(new Runnable() {
@Override
public void run() {
if (mBackgroundURI != null) {
updateBackground(mBackgroundURI);
}
}
});
}
}
private class GridItemPresenter extends Presenter {
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent) {
TextView view = new TextView(parent.getContext());
view.setLayoutParams(new ViewGroup.LayoutParams(GRID_ITEM_WIDTH, GRID_ITEM_HEIGHT));
view.setFocusable(true);
view.setFocusableInTouchMode(true);
view.setBackgroundColor(getResources().getColor(R.color.default_background));
view.setTextColor(Color.WHITE);
view.setGravity(Gravity.CENTER);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ViewHolder viewHolder, Object item) {
((TextView) viewHolder.view).setText((String) item);
}
@Override
public void onUnbindViewHolder(ViewHolder viewHolder) {
}
}
private void updateRecommendations() {
Intent recommendationIntent = new Intent(getActivity(), UpdateRecommendationsService.class);
getActivity().startService(recommendationIntent);
}
}

View file

@ -0,0 +1,143 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.example.android.leanback;
import android.util.Log;
import java.io.Serializable;
import java.net.URI;
import java.net.URISyntaxException;
/*
* Movie class represents video entity with title, description, image thumbs and video url.
*
*/
public class Movie implements Serializable {
static final long serialVersionUID = 727566175075960653L;
private static long count = 0;
private long id;
private String title;
private String description;
private String bgImageUrl;
private String cardImageUrl;
private String videoUrl;
private String studio;
private String category;
public Movie() {
}
public static long getCount() {
return count;
}
public static void incCount() {
count++;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getStudio() {
return studio;
}
public void setStudio(String studio) {
this.studio = studio;
}
public String getVideoUrl() {
return videoUrl;
}
public void setVideoUrl(String videoUrl) {
this.videoUrl = videoUrl;
}
public String getBackgroundImageUrl() {
return bgImageUrl;
}
public void setBackgroundImageUrl(String bgImageUrl) {
this.bgImageUrl = bgImageUrl;
}
public String getCardImageUrl() {
return cardImageUrl;
}
public void setCardImageUrl(String cardImageUrl) {
this.cardImageUrl = cardImageUrl;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public URI getBackgroundImageURI() {
try {
Log.d("BACK MOVIE: ", bgImageUrl);
return new URI(getBackgroundImageUrl());
} catch (URISyntaxException e) {
Log.d("URI exception: ", bgImageUrl);
return null;
}
}
public URI getCardImageURI() {
try {
return new URI(getCardImageUrl());
} catch (URISyntaxException e) {
return null;
}
}
@Override
public String toString() {
return "Movie{" +
"id=" + id +
", title='" + title + '\'' +
", videoUrl='" + videoUrl + '\'' +
", backgroundImageUrl='" + bgImageUrl + '\'' +
", backgroundImageURI='" + getBackgroundImageURI().toString() + '\'' +
", cardImageUrl='" + cardImageUrl + '\'' +
'}';
}
}

View file

@ -0,0 +1,67 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.example.android.leanback;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.support.v17.leanback.app.BackgroundManager;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;
/**
* Picasso target for updating default_background images
*/
public class PicassoBackgroundManagerTarget implements Target {
BackgroundManager mBackgroundManager;
public PicassoBackgroundManagerTarget(BackgroundManager backgroundManager) {
this.mBackgroundManager = backgroundManager;
}
@Override
public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom loadedFrom) {
this.mBackgroundManager.setBitmap(bitmap);
}
@Override
public void onBitmapFailed(Drawable drawable) {
this.mBackgroundManager.setDrawable(drawable);
}
@Override
public void onPrepareLoad(Drawable drawable) {
// Do nothing, default_background manager has its own transitions
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
PicassoBackgroundManagerTarget that = (PicassoBackgroundManagerTarget) o;
if (!mBackgroundManager.equals(that.mBackgroundManager))
return false;
return true;
}
@Override
public int hashCode() {
return mBackgroundManager.hashCode();
}
}

View file

@ -0,0 +1,451 @@
/*
* Copyright (C) 2013 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 com.example.android.leanback;
import android.app.Activity;
import android.content.Intent;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
import android.media.MediaPlayer.OnErrorListener;
import android.media.MediaPlayer.OnPreparedListener;
import android.os.Bundle;
import android.os.Handler;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MenuItem;
import android.view.View;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.RelativeLayout.LayoutParams;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.VideoView;
import java.util.Locale;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
/*
* PlayerActivity handles video playback
*/
public class PlayerActivity extends Activity {
private static final String TAG = "PlayerActivity";
private static final int HIDE_CONTROLLER_TIME = 5000;
private static final int SEEKBAR_DELAY_TIME = 100;
private static final int SEEKBAR_INTERVAL_TIME = 1000;
private static final int MIN_SCRUB_TIME = 3000;
private static final int SCRUB_SEGMENT_DIVISOR = 30;
private static final double MEDIA_BAR_TOP_MARGIN = 0.8;
private static final double MEDIA_BAR_RIGHT_MARGIN = 0.2;
private static final double MEDIA_BAR_BOTTOM_MARGIN = 0.0;
private static final double MEDIA_BAR_LEFT_MARGIN = 0.2;
private static final double MEDIA_BAR_HEIGHT = 0.1;
private static final double MEDIA_BAR_WIDTH = 0.9;
private VideoView mVideoView;
private TextView mStartText;
private TextView mEndText;
private SeekBar mSeekbar;
private ImageView mPlayPause;
private ProgressBar mLoading;
private View mControllers;
private View mContainer;
private Timer mSeekbarTimer;
private Timer mControllersTimer;
private PlaybackState mPlaybackState;
private final Handler mHandler = new Handler();
private Movie mSelectedMovie;
private boolean mShouldStartPlayback;
private boolean mControllersVisible;
private int mDuration;
private DisplayMetrics mMetrics;
/*
* List of various states that we can be in
*/
public static enum PlaybackState {
PLAYING, PAUSED, BUFFERING, IDLE;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.player_activity);
mMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(mMetrics);
loadViews();
setupController();
setupControlsCallbacks();
startVideoPlayer();
updateMetadata(true);
}
private void startVideoPlayer() {
Bundle b = getIntent().getExtras();
mSelectedMovie = (Movie) getIntent().getSerializableExtra(
getResources().getString(R.string.movie));
if (null != b) {
mShouldStartPlayback = b.getBoolean(getResources().getString(R.string.should_start));
int startPosition = b.getInt(getResources().getString(R.string.start_position), 0);
mVideoView.setVideoPath(mSelectedMovie.getVideoUrl());
if (mShouldStartPlayback) {
mPlaybackState = PlaybackState.PLAYING;
updatePlayButton(mPlaybackState);
if (startPosition > 0) {
mVideoView.seekTo(startPosition);
}
mVideoView.start();
mPlayPause.requestFocus();
startControllersTimer();
} else {
updatePlaybackLocation();
mPlaybackState = PlaybackState.PAUSED;
updatePlayButton(mPlaybackState);
}
}
}
private void updatePlaybackLocation() {
if (mPlaybackState == PlaybackState.PLAYING ||
mPlaybackState == PlaybackState.BUFFERING) {
startControllersTimer();
} else {
stopControllersTimer();
}
}
private void play(int position) {
startControllersTimer();
mVideoView.seekTo(position);
mVideoView.start();
restartSeekBarTimer();
}
private void stopSeekBarTimer() {
if (null != mSeekbarTimer) {
mSeekbarTimer.cancel();
}
}
private void restartSeekBarTimer() {
stopSeekBarTimer();
mSeekbarTimer = new Timer();
mSeekbarTimer.scheduleAtFixedRate(new UpdateSeekbarTask(), SEEKBAR_DELAY_TIME,
SEEKBAR_INTERVAL_TIME);
}
private void stopControllersTimer() {
if (null != mControllersTimer) {
mControllersTimer.cancel();
}
}
private void startControllersTimer() {
if (null != mControllersTimer) {
mControllersTimer.cancel();
}
mControllersTimer = new Timer();
mControllersTimer.schedule(new HideControllersTask(), HIDE_CONTROLLER_TIME);
}
private void updateControllersVisibility(boolean show) {
if (show) {
mControllers.setVisibility(View.VISIBLE);
} else {
mControllers.setVisibility(View.INVISIBLE);
}
}
@Override
protected void onPause() {
super.onPause();
Log.d(TAG, "onPause() was called");
if (null != mSeekbarTimer) {
mSeekbarTimer.cancel();
mSeekbarTimer = null;
}
if (null != mControllersTimer) {
mControllersTimer.cancel();
}
mVideoView.pause();
mPlaybackState = PlaybackState.PAUSED;
updatePlayButton(PlaybackState.PAUSED);
}
@Override
protected void onStop() {
Log.d(TAG, "onStop() was called");
super.onStop();
}
@Override
protected void onDestroy() {
Log.d(TAG, "onDestroy() is called");
stopControllersTimer();
stopSeekBarTimer();
super.onDestroy();
}
@Override
protected void onStart() {
Log.d(TAG, "onStart() was called");
super.onStart();
}
@Override
protected void onResume() {
Log.d(TAG, "onResume() was called");
super.onResume();
}
private class HideControllersTask extends TimerTask {
@Override
public void run() {
mHandler.post(new Runnable() {
@Override
public void run() {
updateControllersVisibility(false);
mControllersVisible = false;
}
});
}
}
private class UpdateSeekbarTask extends TimerTask {
@Override
public void run() {
mHandler.post(new Runnable() {
@Override
public void run() {
int currentPos = 0;
currentPos = mVideoView.getCurrentPosition();
updateSeekbar(currentPos, mDuration);
}
});
}
}
private class BackToDetailTask extends TimerTask {
@Override
public void run() {
mHandler.post(new Runnable() {
@Override
public void run() {
Intent intent = new Intent(PlayerActivity.this, DetailsActivity.class);
intent.putExtra(getResources().getString(R.string.movie), mSelectedMovie);
startActivity(intent);
}
});
}
}
private void setupController() {
int w = (int) (mMetrics.widthPixels * MEDIA_BAR_WIDTH);
int h = (int) (mMetrics.heightPixels * MEDIA_BAR_HEIGHT);
int marginLeft = (int) (mMetrics.widthPixels * MEDIA_BAR_LEFT_MARGIN);
int marginTop = (int) (mMetrics.heightPixels * MEDIA_BAR_TOP_MARGIN);
int marginRight = (int) (mMetrics.widthPixels * MEDIA_BAR_RIGHT_MARGIN);
int marginBottom = (int) (mMetrics.heightPixels * MEDIA_BAR_BOTTOM_MARGIN);
LayoutParams lp = new LayoutParams(w, h);
lp.setMargins(marginLeft, marginTop, marginRight, marginBottom);
mControllers.setLayoutParams(lp);
mStartText.setText(getResources().getString(R.string.init_text));
mEndText.setText(getResources().getString(R.string.init_text));
}
private void setupControlsCallbacks() {
mVideoView.setOnErrorListener(new OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
String msg = "";
if (extra == MediaPlayer.MEDIA_ERROR_TIMED_OUT) {
msg = getString(R.string.video_error_media_load_timeout);
} else if (what == MediaPlayer.MEDIA_ERROR_SERVER_DIED) {
msg = getString(R.string.video_error_server_unaccessible);
} else {
msg = getString(R.string.video_error_unknown_error);
}
Utils.showErrorDialog(PlayerActivity.this, msg);
mVideoView.stopPlayback();
mPlaybackState = PlaybackState.IDLE;
return false;
}
});
mVideoView.setOnPreparedListener(new OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
Log.d(TAG, "onPrepared is reached");
mDuration = mp.getDuration();
mEndText.setText(formatTimeSignature(mDuration));
mSeekbar.setMax(mDuration);
restartSeekBarTimer();
}
});
mVideoView.setOnCompletionListener(new OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
stopSeekBarTimer();
mPlaybackState = PlaybackState.IDLE;
updatePlayButton(PlaybackState.IDLE);
mControllersTimer = new Timer();
mControllersTimer.schedule(new BackToDetailTask(), HIDE_CONTROLLER_TIME);
}
});
}
/*
* @Override public boolean onKeyDown(int keyCode, KeyEvent event) { return
* super.onKeyDown(keyCode, event); }
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
int currentPos = 0;
int delta = (int) (mDuration / SCRUB_SEGMENT_DIVISOR);
if (delta < MIN_SCRUB_TIME)
delta = MIN_SCRUB_TIME;
Log.v("keycode", "duration " + mDuration + " delta:" + delta);
if (!mControllersVisible) {
updateControllersVisibility(true);
}
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_CENTER:
return true;
case KeyEvent.KEYCODE_DPAD_DOWN:
return true;
case KeyEvent.KEYCODE_DPAD_LEFT:
currentPos = mVideoView.getCurrentPosition();
currentPos -= delta;
if (currentPos > 0)
play(currentPos);
return true;
case KeyEvent.KEYCODE_DPAD_RIGHT:
currentPos = mVideoView.getCurrentPosition();
currentPos += delta;
if (currentPos < mDuration)
play(currentPos);
return true;
case KeyEvent.KEYCODE_DPAD_UP:
return true;
}
return super.onKeyDown(keyCode, event);
}
private void updateSeekbar(int position, int duration) {
mSeekbar.setProgress(position);
mSeekbar.setMax(duration);
mStartText.setText(formatTimeSignature(mDuration));
}
private void updatePlayButton(PlaybackState state) {
switch (state) {
case PLAYING:
mLoading.setVisibility(View.INVISIBLE);
mPlayPause.setVisibility(View.VISIBLE);
mPlayPause.setImageDrawable(
getResources().getDrawable(R.drawable.ic_pause_playcontrol_normal));
break;
case PAUSED:
case IDLE:
mLoading.setVisibility(View.INVISIBLE);
mPlayPause.setVisibility(View.VISIBLE);
mPlayPause.setImageDrawable(
getResources().getDrawable(R.drawable.ic_play_playcontrol_normal));
break;
case BUFFERING:
mPlayPause.setVisibility(View.INVISIBLE);
mLoading.setVisibility(View.VISIBLE);
break;
default:
break;
}
}
private void updateMetadata(boolean visible) {
mVideoView.invalidate();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
return true;
}
private void loadViews() {
mVideoView = (VideoView) findViewById(R.id.videoView);
mStartText = (TextView) findViewById(R.id.startText);
mEndText = (TextView) findViewById(R.id.endText);
mSeekbar = (SeekBar) findViewById(R.id.seekBar);
mPlayPause = (ImageView) findViewById(R.id.playpause);
mLoading = (ProgressBar) findViewById(R.id.progressBar);
mControllers = findViewById(R.id.controllers);
mContainer = findViewById(R.id.container);
mVideoView.setOnClickListener(mPlayPauseHandler);
}
View.OnClickListener mPlayPauseHandler = new View.OnClickListener() {
public void onClick(View v) {
Log.d(TAG, "clicked play pause button");
if (!mControllersVisible) {
updateControllersVisibility(true);
}
if (mPlaybackState == PlaybackState.PAUSED) {
mPlaybackState = PlaybackState.PLAYING;
updatePlayButton(mPlaybackState);
mVideoView.start();
startControllersTimer();
} else {
mVideoView.pause();
mPlaybackState = PlaybackState.PAUSED;
updatePlayButton(PlaybackState.PAUSED);
stopControllersTimer();
}
}
};
private String formatTimeSignature(int timeSignature) {
return String.format(Locale.US,
"%02d:%02d",
TimeUnit.MILLISECONDS.toMinutes(timeSignature),
TimeUnit.MILLISECONDS.toSeconds(timeSignature)
-
TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS
.toMinutes(timeSignature)));
}
}

View file

@ -0,0 +1,153 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.example.android.leanback;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import com.squareup.picasso.Picasso;
import java.io.IOException;
/*
* This class builds recommendations as notifications with videos as inputs.
*/
public class RecommendationBuilder {
private static final String TAG = "RecommendationBuilder";
private static int CARD_WIDTH = 313;
private static int CARD_HEIGHT = 176;
public static final String EXTRA_BACKGROUND_IMAGE_URL = "background_image_url";
private Context mContext;
private NotificationManager mNotificationManager;
private int mId;
private int mPriority;
private int mSmallIcon;
private String mTitle;
private String mDescription;
private String mImageUri;
private String mBackgroundUri;
private PendingIntent mIntent;
public RecommendationBuilder() {
}
public RecommendationBuilder setContext(Context context) {
mContext = context;
return this;
}
public RecommendationBuilder setId(int id) {
mId = id;
return this;
}
public RecommendationBuilder setPriority(int priority) {
mPriority = priority;
return this;
}
public RecommendationBuilder setTitle(String title) {
mTitle = title;
return this;
}
public RecommendationBuilder setDescription(String description) {
mDescription = description;
return this;
}
public RecommendationBuilder setImage(String uri) {
mImageUri = uri;
return this;
}
public RecommendationBuilder setBackground(String uri) {
mBackgroundUri = uri;
return this;
}
public RecommendationBuilder setIntent(PendingIntent intent) {
mIntent = intent;
return this;
}
public RecommendationBuilder setSmallIcon(int resourceId) {
mSmallIcon = resourceId;
return this;
}
public Notification build() throws IOException {
Log.d(TAG, "Building notification - " + this.toString());
if (mNotificationManager == null) {
mNotificationManager = (NotificationManager) mContext
.getSystemService(Context.NOTIFICATION_SERVICE);
}
Bundle extras = new Bundle();
if (mBackgroundUri != null) {
extras.putString(EXTRA_BACKGROUND_IMAGE_URL, mBackgroundUri);
}
Bitmap image = Picasso.with(mContext)
.load(mImageUri)
.resize(Utils.dpToPx(CARD_WIDTH, mContext), Utils.dpToPx(CARD_HEIGHT, mContext))
.get();
Notification notification = new NotificationCompat.BigPictureStyle(
new NotificationCompat.Builder(mContext)
.setContentTitle(mTitle)
.setContentText(mDescription)
.setPriority(mPriority)
.setLocalOnly(true)
.setOngoing(true)
.setColor(mContext.getResources().getColor(R.color.fastlane_background))
// .setCategory(Notification.CATEGORY_RECOMMENDATION)
.setCategory("recommendation")
.setLargeIcon(image)
.setSmallIcon(mSmallIcon)
.setContentIntent(mIntent)
.setExtras(extras))
.build();
mNotificationManager.notify(mId, notification);
mNotificationManager = null;
return notification;
}
@Override
public String toString() {
return "RecommendationBuilder{" +
", mId=" + mId +
", mPriority=" + mPriority +
", mSmallIcon=" + mSmallIcon +
", mTitle='" + mTitle + '\'' +
", mDescription='" + mDescription + '\'' +
", mImageUri='" + mImageUri + '\'' +
", mBackgroundUri='" + mBackgroundUri + '\'' +
", mIntent=" + mIntent +
'}';
}
}

View file

@ -0,0 +1,32 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.example.android.leanback;
import android.app.Activity;
import android.os.Bundle;
/*
* This class is a wrapper activity for SearchFragment
*/
public class SearchActivity extends Activity
{
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.search);
}
}

View file

@ -0,0 +1,136 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.example.android.leanback;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.support.v17.leanback.widget.ArrayObjectAdapter;
import android.support.v17.leanback.widget.HeaderItem;
import android.support.v17.leanback.widget.ListRow;
import android.support.v17.leanback.widget.ListRowPresenter;
import android.support.v17.leanback.widget.ObjectAdapter;
import android.support.v17.leanback.widget.OnItemClickedListener;
import android.support.v17.leanback.widget.Row;
import android.text.TextUtils;
import android.util.Log;
/*
* This class demonstrates how to do in-app search
*/
@SuppressLint("DefaultLocale")
public class SearchFragment extends android.support.v17.leanback.app.SearchFragment
implements android.support.v17.leanback.app.SearchFragment.SearchResultProvider {
private static final String TAG = "SearchFragment";
private static final int SEARCH_DELAY_MS = 300;
private ArrayObjectAdapter mRowsAdapter;
private Handler mHandler = new Handler();
private SearchRunnable mDelayedLoad;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
setSearchResultProvider(this);
setOnItemClickedListener(getDefaultItemClickedListener());
mDelayedLoad = new SearchRunnable();
}
@Override
public ObjectAdapter getResultsAdapter() {
return mRowsAdapter;
}
private void queryByWords(String words) {
mRowsAdapter.clear();
if (!TextUtils.isEmpty(words)) {
mDelayedLoad.setSearchQuery(words);
mHandler.removeCallbacks(mDelayedLoad);
mHandler.postDelayed(mDelayedLoad, SEARCH_DELAY_MS);
}
}
@Override
public boolean onQueryTextChange(String newQuery) {
Log.i(TAG, String.format("Search Query Text Change %s", newQuery));
queryByWords(newQuery);
return true;
}
@Override
public boolean onQueryTextSubmit(String query) {
Log.i(TAG, String.format("Search Query Text Submit %s", query));
queryByWords(query);
return true;
}
private void loadRows(String query) {
HashMap<String, List<Movie>> movies = VideoProvider.getMovieList();
ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(new CardPresenter());
for (Map.Entry<String, List<Movie>> entry : movies.entrySet())
{
for (int i = 0; i < entry.getValue().size(); i++) {
Movie movie = entry.getValue().get(i);
if (movie.getTitle().toLowerCase(Locale.ENGLISH)
.indexOf(query.toLowerCase(Locale.ENGLISH)) >= 0
|| movie.getDescription().toLowerCase(Locale.ENGLISH)
.indexOf(query.toLowerCase(Locale.ENGLISH)) >= 0) {
listRowAdapter.add(movie);
}
}
}
HeaderItem header = new HeaderItem(0, getResources().getString(R.string.search_results),
null);
mRowsAdapter.add(new ListRow(header, listRowAdapter));
}
protected OnItemClickedListener getDefaultItemClickedListener() {
return new OnItemClickedListener() {
@Override
public void onItemClicked(Object item, Row row) {
if (item instanceof Movie) {
Movie movie = (Movie) item;
Intent intent = new Intent(getActivity(), DetailsActivity.class);
intent.putExtra(getResources().getString(R.string.movie), movie);
startActivity(intent);
}
}
};
}
private class SearchRunnable implements Runnable {
private volatile String searchQuery;
public SearchRunnable() {
}
public void run() {
loadRows(searchQuery);
}
public void setSearchQuery(String value) {
this.searchQuery = value;
}
}
}

View file

@ -0,0 +1,94 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.example.android.leanback;
import android.app.IntentService;
import android.app.PendingIntent;
import android.app.TaskStackBuilder;
import android.content.Intent;
import android.util.Log;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/*
* This class builds up to MAX_RECOMMMENDATIONS of recommendations and defines what happens
* when they're clicked from Recommendations seciton on Home screen
*/
public class UpdateRecommendationsService extends IntentService {
private static final String TAG = "UpdateRecommendationsService";
private static final int MAX_RECOMMENDATIONS = 3;
public UpdateRecommendationsService() {
super("RecommendationService");
}
@Override
protected void onHandleIntent(Intent intent) {
Log.d(TAG, "Updating recommendation cards");
HashMap<String, List<Movie>> recommendations = VideoProvider.getMovieList();
int count = 0;
try {
RecommendationBuilder builder = new RecommendationBuilder()
.setContext(getApplicationContext())
.setSmallIcon(R.drawable.videos_by_google_icon);
for (Map.Entry<String, List<Movie>> entry : recommendations.entrySet())
{
for (int i = 0; i < entry.getValue().size(); i++) {
Movie movie = entry.getValue().get(i);
Log.d(TAG, "Recommendation - " + movie.getTitle());
builder.setBackground(movie.getCardImageUrl())
.setId(count + 1)
.setPriority(MAX_RECOMMENDATIONS - count)
.setTitle(movie.getTitle())
.setDescription(getString(R.string.popular_header))
.setImage(movie.getCardImageUrl())
.setIntent(buildPendingIntent(movie))
.build();
if (++count >= MAX_RECOMMENDATIONS) {
break;
}
}
if (++count >= MAX_RECOMMENDATIONS) {
break;
}
}
} catch (IOException e) {
Log.e(TAG, "Unable to update recommendation", e);
}
}
private PendingIntent buildPendingIntent(Movie movie) {
Intent detailsIntent = new Intent(this, DetailsActivity.class);
detailsIntent.putExtra("Movie", movie);
TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
stackBuilder.addParentStack(DetailsActivity.class);
stackBuilder.addNextIntent(detailsIntent);
// Ensure a unique PendingIntents, otherwise all recommendations end up with the same
// PendingIntent
detailsIntent.setAction(Long.toString(movie.getId()));
PendingIntent intent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
return intent;
}
}

View file

@ -0,0 +1,98 @@
/*
* Copyright (C) 2013 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 com.example.android.leanback;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Point;
import android.view.Display;
import android.view.WindowManager;
import android.widget.Toast;
/**
* A collection of utility methods, all static.
*/
public class Utils {
/*
* Making sure public utility methods remain static
*/
private Utils() {
}
/**
* Returns the screen/display size
*
* @param context
* @return
*/
public static Point getDisplaySize(Context context) {
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
Point size = new Point();
display.getSize(size);
int width = size.x;
int height = size.y;
return new Point(width, height);
}
/**
* Shows an error dialog with a given text message.
*
* @param context
* @param errorString
*/
public static final void showErrorDialog(Context context, String errorString) {
new AlertDialog.Builder(context).setTitle(R.string.error)
.setMessage(errorString)
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
dialog.cancel();
}
})
.create()
.show();
}
/**
* Shows a (long) toast
*
* @param context
* @param msg
*/
public static void showToast(Context context, String msg) {
Toast.makeText(context, msg, Toast.LENGTH_LONG).show();
}
/**
* Shows a (long) toast.
*
* @param context
* @param resourceId
*/
public static void showToast(Context context, int resourceId) {
Toast.makeText(context, context.getString(resourceId), Toast.LENGTH_LONG).show();
}
public static int dpToPx(int dp, Context ctx) {
float density = ctx.getResources().getDisplayMetrics().density;
return Math.round((float) dp * density);
}
}

View file

@ -0,0 +1,33 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.example.android.leanback;
import android.app.Activity;
import android.os.Bundle;
/*
* Wrapper class for VerticalGridFragment
*/
public class VerticalGridActivity extends Activity
{
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.vertical_grid);
getWindow().setBackgroundDrawableResource(R.drawable.grid_bg);
}
}

View file

@ -0,0 +1,90 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.example.android.leanback;
import java.util.*;
import android.content.Intent;
import android.os.Bundle;
import android.support.v17.leanback.widget.ArrayObjectAdapter;
import android.support.v17.leanback.widget.OnItemClickedListener;
import android.support.v17.leanback.widget.OnItemSelectedListener;
import android.support.v17.leanback.widget.Row;
import android.support.v17.leanback.widget.VerticalGridPresenter;
import android.util.Log;
/*
* VerticalGridFragment shows a grid of videos
*/
public class VerticalGridFragment extends android.support.v17.leanback.app.VerticalGridFragment {
private static final String TAG = "VerticalGridFragment";
private static final int NUM_COLUMNS = 5;
private ArrayObjectAdapter mAdapter;
@Override
public void onCreate(Bundle savedInstanceState) {
Log.i(TAG, "onCreate");
super.onCreate(savedInstanceState);
setTitle(getString(R.string.vertical_grid_title));
setupFragment();
}
private void setupFragment() {
VerticalGridPresenter gridPresenter = new VerticalGridPresenter();
gridPresenter.setNumberOfColumns(NUM_COLUMNS);
setGridPresenter(gridPresenter);
mAdapter = new ArrayObjectAdapter(new CardPresenter());
long seed = System.nanoTime();
HashMap<String, List<Movie>> movies = VideoProvider.getMovieList();
for (Map.Entry<String, List<Movie>> entry : movies.entrySet())
{
List<Movie> list = entry.getValue();
Collections.shuffle(list, new Random(seed));
for (int j = 0; j < list.size(); j++) {
mAdapter.add(list.get(j));
}
}
setAdapter(mAdapter);
setOnItemSelectedListener(new OnItemSelectedListener() {
@Override
public void onItemSelected(Object item, Row row) {
}
});
setOnItemClickedListener(new OnItemClickedListener() {
@Override
public void onItemClicked(Object item, Row row) {
if (item instanceof Movie) {
Movie movie = (Movie) item;
Intent intent = new Intent(getActivity(), DetailsActivity.class);
intent.putExtra(getString(R.string.movie), movie);
startActivity(intent);
}
}
});
}
}

View file

@ -0,0 +1,66 @@
/*
* Copyright (C) 2013 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 com.example.android.leanback;
import java.util.HashMap;
import java.util.List;
import android.content.AsyncTaskLoader;
import android.content.Context;
import android.util.Log;
/*
* This class asynchronously loads videos from a backend
*/
public class VideoItemLoader extends AsyncTaskLoader<HashMap<String, List<Movie>>> {
private static final String TAG = "VideoItemLoader";
private final String mUrl;
private Context mContext;
public VideoItemLoader(Context context, String url) {
super(context);
mContext = context;
mUrl = url;
}
@Override
public HashMap<String, List<Movie>> loadInBackground() {
try {
return VideoProvider.buildMedia(mContext, mUrl);
} catch (Exception e) {
Log.e(TAG, "Failed to fetch media data", e);
return null;
}
}
@Override
protected void onStartLoading() {
super.onStartLoading();
forceLoad();
}
/**
* Handles a request to stop the Loader.
*/
@Override
protected void onStopLoading() {
// Attempt to cancel the current load task if possible.
cancelLoad();
}
}

View file

@ -0,0 +1,180 @@
/*
* Copyright (C) 2013 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 com.example.android.leanback;
import android.content.Context;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
/*
* This class loads videos from a backend and saves them into a HashMap
*/
public class VideoProvider {
private static final String TAG = "VideoProvider";
private static String TAG_MEDIA = "videos";
private static String TAG_GOOGLE_VIDEOS = "googlevideos";
private static String TAG_CATEGORY = "category";
private static String TAG_STUDIO = "studio";
private static String TAG_SOURCES = "sources";
private static String TAG_DESCRIPTION = "description";
private static String TAG_CARD_THUMB = "card";
private static String TAG_BACKGROUND = "background";
private static String TAG_TITLE = "title";
private static HashMap<String, List<Movie>> mMovieList;
private static Context mContext;
private static String mPrefixUrl;
public static void setContext(Context context) {
if (mContext == null)
mContext = context;
}
protected JSONObject parseUrl(String urlString) {
Log.d(TAG, "Parse URL: " + urlString);
InputStream is = null;
mPrefixUrl = mContext.getResources().getString(R.string.prefix_url);
try {
java.net.URL url = new java.net.URL(urlString);
URLConnection urlConnection = url.openConnection();
is = new BufferedInputStream(urlConnection.getInputStream());
BufferedReader reader = new BufferedReader(new InputStreamReader(
urlConnection.getInputStream(), "iso-8859-1"), 8);
StringBuilder sb = new StringBuilder();
String line = null;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
String json = sb.toString();
return new JSONObject(json);
} catch (Exception e) {
Log.d(TAG, "Failed to parse the json for media list", e);
return null;
} finally {
if (null != is) {
try {
is.close();
} catch (IOException e) {
Log.d(TAG, "JSON feed closed", e);
}
}
}
}
public static HashMap<String, List<Movie>> getMovieList() {
return mMovieList;
}
public static HashMap<String, List<Movie>> buildMedia(Context ctx, String url)
throws JSONException {
if (null != mMovieList) {
return mMovieList;
}
mMovieList = new HashMap<String, List<Movie>>();
JSONObject jsonObj = new VideoProvider().parseUrl(url);
JSONArray categories = jsonObj.getJSONArray(TAG_GOOGLE_VIDEOS);
if (null != categories) {
Log.d(TAG, "category #: " + categories.length());
String title = new String();
String videoUrl = new String();
String bgImageUrl = new String();
String cardImageUrl = new String();
String studio = new String();
for (int i = 0; i < categories.length(); i++) {
JSONObject category = categories.getJSONObject(i);
String category_name = category.getString(TAG_CATEGORY);
JSONArray videos = category.getJSONArray(TAG_MEDIA);
Log.d(TAG,
"category: " + i + " Name:" + category_name + " video length: "
+ videos.length());
List<Movie> categoryList = new ArrayList<Movie>();
if (null != videos) {
for (int j = 0; j < videos.length(); j++) {
JSONObject video = videos.getJSONObject(j);
String description = video.getString(TAG_DESCRIPTION);
JSONArray videoUrls = video.getJSONArray(TAG_SOURCES);
if (null == videoUrls || videoUrls.length() == 0) {
continue;
}
title = video.getString(TAG_TITLE);
videoUrl = getVideoPrefix(category_name, videoUrls.getString(0));
bgImageUrl = getThumbPrefix(category_name, title,
video.getString(TAG_BACKGROUND));
cardImageUrl = getThumbPrefix(category_name, title,
video.getString(TAG_CARD_THUMB));
studio = video.getString(TAG_STUDIO);
categoryList.add(buildMovieInfo(category_name, title, description, studio,
videoUrl, cardImageUrl,
bgImageUrl));
}
mMovieList.put(category_name, categoryList);
}
}
}
return mMovieList;
}
private static Movie buildMovieInfo(String category, String title,
String description, String studio, String videoUrl, String cardImageUrl,
String bgImageUrl) {
Movie movie = new Movie();
movie.setId(Movie.getCount());
Movie.incCount();
movie.setTitle(title);
movie.setDescription(description);
movie.setStudio(studio);
movie.setCategory(category);
movie.setCardImageUrl(cardImageUrl);
movie.setBackgroundImageUrl(bgImageUrl);
movie.setVideoUrl(videoUrl);
return movie;
}
private static String getVideoPrefix(String category, String videoUrl) {
String ret = "";
ret = mPrefixUrl + category.replace(" ", "%20") + '/' +
videoUrl.replace(" ", "%20");
return ret;
}
private static String getThumbPrefix(String category, String title, String imageUrl) {
String ret = "";
ret = mPrefixUrl + category.replace(" ", "%20") + '/' +
title.replace(" ", "%20") + '/' +
imageUrl.replace(" ", "%20");
return ret;
}
}

View file

@ -0,0 +1,67 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.tvleanback;
import android.content.Context;
import android.graphics.Point;
import android.view.Display;
import android.view.WindowManager;
import android.widget.Toast;
/**
* A collection of utility methods, all static.
*/
public class Utils {
/*
* Making sure public utility methods remain static
*/
private Utils() {
}
/**
* Returns the screen/display size
*/
public static Point getDisplaySize(Context context) {
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
Point size = new Point();
display.getSize(size);
int width = size.x;
int height = size.y;
return size;
}
/**
* Shows a (long) toast
*/
public static void showToast(Context context, String msg) {
Toast.makeText(context, msg, Toast.LENGTH_LONG).show();
}
/**
* Shows a (long) toast.
*/
public static void showToast(Context context, int resourceId) {
Toast.makeText(context, context.getString(resourceId), Toast.LENGTH_LONG).show();
}
public static int convertDpToPixel(Context ctx, int dp) {
float density = ctx.getResources().getDisplayMetrics().density;
return Math.round((float) dp * density);
}
}

View file

@ -0,0 +1,221 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.tvleanback.data;
import android.database.AbstractCursor;
import android.database.Cursor;
/**
* A sample paginated cursor which will pre-fetch and cache rows.
*/
public class PaginatedCursor extends AbstractCursor {
/**
* The number of items that should be loaded each time.
*/
private static final int PAGE_SIZE = 10;
/**
* The threshold of number of items left that a new page should be loaded.
*/
private static final int PAGE_THRESHOLD = PAGE_SIZE / 2;
private final Cursor mCursor;
private final int mRowCount;
private final boolean[] mCachedRows;
private final String[] mColumnNames;
private final int mColumnCount;
private final int[] mColumnTypes;
private final byte[][][] mByteArrayDataCache;
private final float[][] mFloatDataCache;
private final int[][] mIntDataCache;
private final String[][] mStringDataCache;
/**
* Index mapping from column index into the data type specific cache index;
*/
private final int[] mByteArrayCacheIndexMap;
private final int[] mFloatCacheIndexMap;
private final int[] mIntCacheIndexMap;
private final int[] mStringCacheIndexMap;
private int mByteArrayCacheColumnSize;
private int mFloatCacheColumnSize;
private int mIntCacheColumnSize;
private int mStringCacheColumnSize;
private int mLastCachePosition;
public PaginatedCursor(Cursor cursor) {
super();
mCursor = cursor;
mRowCount = mCursor.getCount();
mCachedRows = new boolean[mRowCount];
mColumnNames = mCursor.getColumnNames();
mColumnCount = mCursor.getColumnCount();
mColumnTypes = new int[mColumnCount];
mByteArrayCacheColumnSize = 0;
mFloatCacheColumnSize = 0;
mIntCacheColumnSize = 0;
mStringCacheColumnSize = 0;
mByteArrayCacheIndexMap = new int[mColumnCount];
mFloatCacheIndexMap = new int[mColumnCount];
mIntCacheIndexMap = new int[mColumnCount];
mStringCacheIndexMap = new int[mColumnCount];
mCursor.moveToFirst();
for (int i = 0; i < mColumnCount; i++) {
int type = mCursor.getType(i);
mColumnTypes[i] = type;
switch (type) {
case Cursor.FIELD_TYPE_BLOB:
mByteArrayCacheIndexMap[i] = mByteArrayCacheColumnSize++;
break;
case Cursor.FIELD_TYPE_FLOAT:
mFloatCacheIndexMap[i] = mFloatCacheColumnSize++;
break;
case Cursor.FIELD_TYPE_INTEGER:
mIntCacheIndexMap[i] = mIntCacheColumnSize++;
break;
case Cursor.FIELD_TYPE_STRING:
mStringCacheIndexMap[i] = mStringCacheColumnSize++;
break;
}
}
mByteArrayDataCache = mByteArrayCacheColumnSize > 0 ? new byte[mRowCount][][] : null;
mFloatDataCache = mFloatCacheColumnSize > 0 ? new float[mRowCount][] : null;
mIntDataCache = mIntCacheColumnSize > 0 ? new int[mRowCount][] : null;
mStringDataCache = mStringCacheColumnSize > 0 ? new String[mRowCount][] : null;
for (int i = 0; i < mRowCount; i++) {
mCachedRows[i] = false;
if (mByteArrayDataCache != null) {
mByteArrayDataCache[i] = new byte[mByteArrayCacheColumnSize][];
}
if (mFloatDataCache != null) {
mFloatDataCache[i] = new float[mFloatCacheColumnSize];
}
if (mIntDataCache != null) {
mIntDataCache[i] = new int[mIntCacheColumnSize];
}
if (mStringDataCache != null) {
mStringDataCache[i] = new String[mStringCacheColumnSize];
}
}
// Cache at the initialization stage.
loadCacheStartingFromPosition(0);
}
/**
* Try to load un-cached data with size {@link PAGE_SIZE} starting from given index.
*/
private void loadCacheStartingFromPosition(int index) {
mCursor.moveToPosition(index);
for (int row = index; row < (index + PAGE_SIZE) && row < mRowCount; row++) {
if (!mCachedRows[row]) {
for (int col = 0; col < mColumnCount; col++) {
switch (mCursor.getType(col)) {
case Cursor.FIELD_TYPE_BLOB:
mByteArrayDataCache[row][mByteArrayCacheIndexMap[col]] =
mCursor.getBlob(col);
break;
case Cursor.FIELD_TYPE_FLOAT:
mFloatDataCache[row][mFloatCacheIndexMap[col]] = mCursor.getFloat(col);
break;
case Cursor.FIELD_TYPE_INTEGER:
mIntDataCache[row][mIntCacheIndexMap[col]] = mCursor.getInt(col);
break;
case Cursor.FIELD_TYPE_STRING:
mStringDataCache[row][mStringCacheIndexMap[col]] =
mCursor.getString(col);
break;
}
}
mCachedRows[row] = true;
}
mCursor.moveToNext();
}
mLastCachePosition = Math.min(index + PAGE_SIZE, mRowCount) - 1;
}
@Override
public boolean onMove(int oldPosition, int newPosition) {
// If it's a consecutive move and haven't exceeds the threshold, do nothing.
if ((newPosition - oldPosition) != 1 ||
(newPosition + PAGE_THRESHOLD) <= mLastCachePosition) {
loadCacheStartingFromPosition(newPosition);
}
return true;
}
@Override
public int getType(int column) {
return mColumnTypes[column];
}
@Override
public int getCount() {
return mRowCount;
}
@Override
public String[] getColumnNames() {
return mColumnNames;
}
@Override
public String getString(int column) {
return mStringDataCache[mPos][mStringCacheIndexMap[column]];
}
@Override
public short getShort(int column) {
return (short) mIntDataCache[mPos][mIntCacheIndexMap[column]];
}
@Override
public int getInt(int column) {
return mIntDataCache[mPos][mIntCacheIndexMap[column]];
}
@Override
public long getLong(int column) {
return mIntDataCache[mPos][mIntCacheIndexMap[column]];
}
@Override
public float getFloat(int column) {
return mFloatDataCache[mPos][mFloatCacheIndexMap[column]];
}
@Override
public double getDouble(int column) {
return mFloatDataCache[mPos][mFloatCacheIndexMap[column]];
}
@Override
public byte[] getBlob(int column) {
return mByteArrayDataCache[mPos][mByteArrayCacheIndexMap[column]];
}
@Override
public boolean isNull(int column) {
return mColumnTypes[column] == Cursor.FIELD_TYPE_NULL;
}
}

View file

@ -0,0 +1,142 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.tvleanback.data;
import android.app.SearchManager;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.net.Uri;
import android.provider.BaseColumns;
import android.util.Log;
/**
* Provides access to the video database.
*/
public class VideoContentProvider extends ContentProvider {
private static String TAG = "VideoContentProvider";
public static String AUTHORITY = "com.example.android.tvleanback";
// UriMatcher stuff
private static final int SEARCH_SUGGEST = 0;
private static final int REFRESH_SHORTCUT = 1;
private static final UriMatcher URI_MATCHER = buildUriMatcher();
private VideoDatabase mVideoDatabase;
/**
* Builds up a UriMatcher for search suggestion and shortcut refresh queries.
*/
private static UriMatcher buildUriMatcher() {
UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
// to get suggestions...
Log.d(TAG, "suggest_uri_path_query: " + SearchManager.SUGGEST_URI_PATH_QUERY);
matcher.addURI(AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, SEARCH_SUGGEST);
matcher.addURI(AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", SEARCH_SUGGEST);
return matcher;
}
@Override
public boolean onCreate() {
Log.d(TAG, "onCreate");
mVideoDatabase = new VideoDatabase(getContext());
return true;
}
/**
* Handles all the video searches and suggestion queries from the Search Manager.
* When requesting a specific word, the uri alone is required.
* When searching all of the video for matches, the selectionArgs argument must carry
* the search query as the first element.
* All other arguments are ignored.
*/
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
// Use the UriMatcher to see what kind of query we have and format the db query accordingly
switch (URI_MATCHER.match(uri)) {
case SEARCH_SUGGEST:
Log.d(TAG, "search suggest: " + selectionArgs[0] + " URI: " + uri);
if (selectionArgs == null) {
throw new IllegalArgumentException(
"selectionArgs must be provided for the Uri: " + uri);
}
return getSuggestions(selectionArgs[0]);
default:
throw new IllegalArgumentException("Unknown Uri: " + uri);
}
}
private Cursor getSuggestions(String query) {
query = query.toLowerCase();
String[] columns = new String[]{
BaseColumns._ID,
VideoDatabase.KEY_NAME,
VideoDatabase.KEY_DESCRIPTION,
VideoDatabase.KEY_ICON,
VideoDatabase.KEY_DATA_TYPE,
VideoDatabase.KEY_IS_LIVE,
VideoDatabase.KEY_VIDEO_WIDTH,
VideoDatabase.KEY_VIDEO_HEIGHT,
VideoDatabase.KEY_AUDIO_CHANNEL_CONFIG,
VideoDatabase.KEY_PURCHASE_PRICE,
VideoDatabase.KEY_RENTAL_PRICE,
VideoDatabase.KEY_RATING_STYLE,
VideoDatabase.KEY_RATING_SCORE,
VideoDatabase.KEY_PRODUCTION_YEAR,
VideoDatabase.KEY_COLUMN_DURATION,
VideoDatabase.KEY_ACTION,
SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID
};
return mVideoDatabase.getWordMatch(query, columns);
}
/**
* This method is required in order to query the supported types.
* It's also useful in our own query() method to determine the type of Uri received.
*/
@Override
public String getType(Uri uri) {
switch (URI_MATCHER.match(uri)) {
case SEARCH_SUGGEST:
return SearchManager.SUGGEST_MIME_TYPE;
case REFRESH_SHORTCUT:
return SearchManager.SHORTCUT_MIME_TYPE;
default:
throw new IllegalArgumentException("Unknown URL " + uri);
}
}
// Other required implementations...
@Override
public Uri insert(Uri uri, ContentValues values) {
throw new UnsupportedOperationException();
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
throw new UnsupportedOperationException();
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
throw new UnsupportedOperationException();
}
}

View file

@ -0,0 +1,344 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.tvleanback.data;
import android.app.SearchManager;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.media.Rating;
import android.provider.BaseColumns;
import android.util.Log;
import com.example.android.tvleanback.R;
import com.example.android.tvleanback.model.Movie;
import org.json.JSONException;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Contains logic to return specific words from the video database, and
* load the video database table when it needs to be created.
*/
public class VideoDatabase {
//The columns we'll include in the video database table
public static final String KEY_NAME = SearchManager.SUGGEST_COLUMN_TEXT_1;
public static final String KEY_DESCRIPTION = SearchManager.SUGGEST_COLUMN_TEXT_2;
public static final String KEY_ICON = SearchManager.SUGGEST_COLUMN_RESULT_CARD_IMAGE;
public static final String KEY_DATA_TYPE = SearchManager.SUGGEST_COLUMN_CONTENT_TYPE;
public static final String KEY_IS_LIVE = SearchManager.SUGGEST_COLUMN_IS_LIVE;
public static final String KEY_VIDEO_WIDTH = SearchManager.SUGGEST_COLUMN_VIDEO_WIDTH;
public static final String KEY_VIDEO_HEIGHT = SearchManager.SUGGEST_COLUMN_VIDEO_HEIGHT;
public static final String KEY_AUDIO_CHANNEL_CONFIG =
SearchManager.SUGGEST_COLUMN_AUDIO_CHANNEL_CONFIG;
public static final String KEY_PURCHASE_PRICE = SearchManager.SUGGEST_COLUMN_PURCHASE_PRICE;
public static final String KEY_RENTAL_PRICE = SearchManager.SUGGEST_COLUMN_RENTAL_PRICE;
public static final String KEY_RATING_STYLE = SearchManager.SUGGEST_COLUMN_RATING_STYLE;
public static final String KEY_RATING_SCORE = SearchManager.SUGGEST_COLUMN_RATING_SCORE;
public static final String KEY_PRODUCTION_YEAR = SearchManager.SUGGEST_COLUMN_PRODUCTION_YEAR;
public static final String KEY_COLUMN_DURATION = SearchManager.SUGGEST_COLUMN_DURATION;
public static final String KEY_ACTION = SearchManager.SUGGEST_COLUMN_INTENT_ACTION;
private static final String TAG = "VideoDatabase";
private static final String DATABASE_NAME = "video_database_leanback";
private static final String FTS_VIRTUAL_TABLE = "Leanback_table";
private static final int DATABASE_VERSION = 2;
private static final HashMap<String, String> COLUMN_MAP = buildColumnMap();
private static int CARD_WIDTH = 313;
private static int CARD_HEIGHT = 176;
private final VideoDatabaseOpenHelper mDatabaseOpenHelper;
/**
* Constructor
*
* @param context The Context within which to work, used to create the DB
*/
public VideoDatabase(Context context) {
mDatabaseOpenHelper = new VideoDatabaseOpenHelper(context);
}
/**
* Builds a map for all columns that may be requested, which will be given to the
* SQLiteQueryBuilder. This is a good way to define aliases for column names, but must include
* all columns, even if the value is the key. This allows the ContentProvider to request
* columns w/o the need to know real column names and create the alias itself.
*/
private static HashMap<String, String> buildColumnMap() {
HashMap<String, String> map = new HashMap<String, String>();
map.put(KEY_NAME, KEY_NAME);
map.put(KEY_DESCRIPTION, KEY_DESCRIPTION);
map.put(KEY_ICON, KEY_ICON);
map.put(KEY_DATA_TYPE, KEY_DATA_TYPE);
map.put(KEY_IS_LIVE, KEY_IS_LIVE);
map.put(KEY_VIDEO_WIDTH, KEY_VIDEO_WIDTH);
map.put(KEY_VIDEO_HEIGHT, KEY_VIDEO_HEIGHT);
map.put(KEY_AUDIO_CHANNEL_CONFIG, KEY_AUDIO_CHANNEL_CONFIG);
map.put(KEY_PURCHASE_PRICE, KEY_PURCHASE_PRICE);
map.put(KEY_RENTAL_PRICE, KEY_RENTAL_PRICE);
map.put(KEY_RATING_STYLE, KEY_RATING_STYLE);
map.put(KEY_RATING_SCORE, KEY_RATING_SCORE);
map.put(KEY_PRODUCTION_YEAR, KEY_PRODUCTION_YEAR);
map.put(KEY_COLUMN_DURATION, KEY_COLUMN_DURATION);
map.put(KEY_ACTION, KEY_ACTION);
map.put(BaseColumns._ID, "rowid AS " +
BaseColumns._ID);
map.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID, "rowid AS " +
SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
map.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, "rowid AS " +
SearchManager.SUGGEST_COLUMN_SHORTCUT_ID);
return map;
}
/**
* Returns a Cursor positioned at the word specified by rowId
*
* @param rowId id of word to retrieve
* @param columns The columns to include, if null then all are included
* @return Cursor positioned to matching word, or null if not found.
*/
public Cursor getWord(String rowId, String[] columns) {
/* This builds a query that looks like:
* SELECT <columns> FROM <table> WHERE rowid = <rowId>
*/
String selection = "rowid = ?";
String[] selectionArgs = new String[]{rowId};
return query(selection, selectionArgs, columns);
}
/**
* Returns a Cursor over all words that match the first letter of the given query
*
* @param query The string to search for
* @param columns The columns to include, if null then all are included
* @return Cursor over all words that match, or null if none found.
*/
public Cursor getWordMatch(String query, String[] columns) {
/* This builds a query that looks like:
* SELECT <columns> FROM <table> WHERE <KEY_WORD> MATCH 'query*'
* which is an FTS3 search for the query text (plus a wildcard) inside the word column.
*
* - "rowid" is the unique id for all rows but we need this value for the "_id" column in
* order for the Adapters to work, so the columns need to make "_id" an alias for "rowid"
* - "rowid" also needs to be used by the SUGGEST_COLUMN_INTENT_DATA alias in order
* for suggestions to carry the proper intent data.SearchManager
* These aliases are defined in the VideoProvider when queries are made.
* - This can be revised to also search the definition text with FTS3 by changing
* the selection clause to use FTS_VIRTUAL_TABLE instead of KEY_WORD (to search across
* the entire table, but sorting the relevance could be difficult.
*/
String selection = KEY_NAME + " MATCH ?";
String[] selectionArgs = new String[]{query + "*"};
return query(selection, selectionArgs, columns);
}
/**
* Performs a database query.
*
* @param selection The selection clause
* @param selectionArgs Selection arguments for "?" components in the selection
* @param columns The columns to return
* @return A Cursor over all rows matching the query
*/
private Cursor query(String selection, String[] selectionArgs, String[] columns) {
/* The SQLiteBuilder provides a map for all possible columns requested to
* actual columns in the database, creating a simple column alias mechanism
* by which the ContentProvider does not need to know the real column names
*/
SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
builder.setTables(FTS_VIRTUAL_TABLE);
builder.setProjectionMap(COLUMN_MAP);
Cursor cursor = new PaginatedCursor(builder.query(mDatabaseOpenHelper.getReadableDatabase(),
columns, selection, selectionArgs, null, null, null));
if (cursor == null) {
return null;
} else if (!cursor.moveToFirst()) {
cursor.close();
return null;
}
return cursor;
}
/**
* This creates/opens the database.
*/
private static class VideoDatabaseOpenHelper extends SQLiteOpenHelper {
private final Context mHelperContext;
private SQLiteDatabase mDatabase;
VideoDatabaseOpenHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
mHelperContext = context;
}
/* Note that FTS3 does not support column constraints and thus, you cannot
* declare a primary key. However, "rowid" is automatically used as a unique
* identifier, so when making requests, we will use "_id" as an alias for "rowid"
*/
private static final String FTS_TABLE_CREATE =
"CREATE VIRTUAL TABLE " + FTS_VIRTUAL_TABLE +
" USING fts3 (" +
KEY_NAME + ", " +
KEY_DESCRIPTION + "," +
KEY_ICON + "," +
KEY_DATA_TYPE + "," +
KEY_IS_LIVE + "," +
KEY_VIDEO_WIDTH + "," +
KEY_VIDEO_HEIGHT + "," +
KEY_AUDIO_CHANNEL_CONFIG + "," +
KEY_PURCHASE_PRICE + "," +
KEY_RENTAL_PRICE + "," +
KEY_RATING_STYLE + "," +
KEY_RATING_SCORE + "," +
KEY_PRODUCTION_YEAR + "," +
KEY_COLUMN_DURATION + "," +
KEY_ACTION + ");";
@Override
public void onCreate(SQLiteDatabase db) {
mDatabase = db;
mDatabase.execSQL(FTS_TABLE_CREATE);
loadDatabase();
}
/**
* Starts a thread to load the database table with words
*/
private void loadDatabase() {
new Thread(new Runnable() {
public void run() {
try {
loadMovies();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}).start();
}
private void loadMovies() throws IOException {
Log.d(TAG, "Loading movies...");
HashMap<String, List<Movie>> movies = null;
try {
VideoProvider.setContext(mHelperContext);
movies = VideoProvider.buildMedia(mHelperContext,
mHelperContext.getResources().getString(R.string.catalog_url));
} catch (JSONException e) {
Log.e(TAG, "JSon Exception when loading movie", e);
}
for (Map.Entry<String, List<Movie>> entry : movies.entrySet()) {
List<Movie> list = entry.getValue();
for (Movie movie : list) {
long id = addMovie(movie);
if (id < 0) {
Log.e(TAG, "unable to add movie: " + movie.toString());
}
}
}
// add dummy movies to illustrate action deep link in search detail
// Android TV Search requires that the medias title, MIME type, production year,
// and duration all match exactly to those found from Googles servers.
addMovieForDeepLink(mHelperContext.getString(R.string.noah_title),
mHelperContext.getString(R.string.noah_description),
R.drawable.noah,
8280000,
"2014");
addMovieForDeepLink(mHelperContext.getString(R.string.dragon2_title),
mHelperContext.getString(R.string.dragon2_description),
R.drawable.dragon2,
6300000,
"2014");
addMovieForDeepLink(mHelperContext.getString(R.string.maleficent_title),
mHelperContext.getString(R.string.maleficent_description),
R.drawable.maleficent,
5820000,
"2014");
}
/**
* Add a movie to the database.
*
* @return rowId or -1 if failed
*/
public long addMovie(Movie movie) {
ContentValues initialValues = new ContentValues();
initialValues.put(KEY_NAME, movie.getTitle());
initialValues.put(KEY_DESCRIPTION, movie.getDescription());
initialValues.put(KEY_ICON, movie.getCardImageUrl());
initialValues.put(KEY_DATA_TYPE, "video/mp4");
initialValues.put(KEY_IS_LIVE, false);
initialValues.put(KEY_VIDEO_WIDTH, CARD_WIDTH);
initialValues.put(KEY_VIDEO_HEIGHT, CARD_HEIGHT);
initialValues.put(KEY_AUDIO_CHANNEL_CONFIG, "2.0");
initialValues.put(KEY_PURCHASE_PRICE, mHelperContext.getString(R.string.buy_2));
initialValues.put(KEY_RENTAL_PRICE, mHelperContext.getString(R.string.rent_2));
initialValues.put(KEY_RATING_STYLE, Rating.RATING_5_STARS);
initialValues.put(KEY_RATING_SCORE, 3.5f);
initialValues.put(KEY_PRODUCTION_YEAR, 2014);
initialValues.put(KEY_COLUMN_DURATION, 0);
initialValues.put(KEY_ACTION, mHelperContext.getString(R.string.global_search));
return mDatabase.insert(FTS_VIRTUAL_TABLE, null, initialValues);
}
/**
* Add an entry to the database for dummy deep link.
*
* @return rowId or -1 if failed
*/
public long addMovieForDeepLink(String title, String description, int icon, long duration, String production_year) {
ContentValues initialValues = new ContentValues();
initialValues.put(KEY_NAME, title);
initialValues.put(KEY_DESCRIPTION, description);
initialValues.put(KEY_ICON, icon);
initialValues.put(KEY_DATA_TYPE, "video/mp4");
initialValues.put(KEY_IS_LIVE, false);
initialValues.put(KEY_VIDEO_WIDTH, 1280);
initialValues.put(KEY_VIDEO_HEIGHT, 720);
initialValues.put(KEY_AUDIO_CHANNEL_CONFIG, "2.0");
initialValues.put(KEY_PURCHASE_PRICE, "Free");
initialValues.put(KEY_RENTAL_PRICE, "Free");
initialValues.put(KEY_RATING_STYLE, Rating.RATING_5_STARS);
initialValues.put(KEY_RATING_SCORE, 3.5f);
initialValues.put(KEY_PRODUCTION_YEAR, production_year);
initialValues.put(KEY_COLUMN_DURATION, duration);
return mDatabase.insert(FTS_VIRTUAL_TABLE, null, initialValues);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
+ newVersion + ", which will destroy all old data");
db.execSQL("DROP TABLE IF EXISTS " + FTS_VIRTUAL_TABLE);
onCreate(db);
}
}
}

View file

@ -0,0 +1,68 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.tvleanback.data;
import android.content.AsyncTaskLoader;
import android.content.Context;
import android.util.Log;
import com.example.android.tvleanback.model.Movie;
import java.util.HashMap;
import java.util.List;
/*
* This class asynchronously loads videos from a backend
*/
public class VideoItemLoader extends AsyncTaskLoader<HashMap<String, List<Movie>>> {
private static final String TAG = "VideoItemLoader";
private final String mUrl;
private Context mContext;
public VideoItemLoader(Context context, String url) {
super(context);
mContext = context;
mUrl = url;
}
@Override
public HashMap<String, List<Movie>> loadInBackground() {
try {
return VideoProvider.buildMedia(mContext, mUrl);
} catch (Exception e) {
Log.e(TAG, "Failed to fetch media data", e);
return null;
}
}
@Override
protected void onStartLoading() {
super.onStartLoading();
forceLoad();
}
/**
* Handles a request to stop the Loader.
*/
@Override
protected void onStopLoading() {
// Attempt to cancel the current load task if possible.
cancelLoad();
}
}

View file

@ -0,0 +1,187 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.tvleanback.data;
import android.content.Context;
import android.util.Log;
import com.example.android.tvleanback.R;
import com.example.android.tvleanback.model.Movie;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
/*
* This class loads videos from a backend and saves them into a HashMap
*/
public class VideoProvider {
private static final String TAG = "VideoProvider";
private static String TAG_MEDIA = "videos";
private static String TAG_GOOGLE_VIDEOS = "googlevideos";
private static String TAG_CATEGORY = "category";
private static String TAG_STUDIO = "studio";
private static String TAG_SOURCES = "sources";
private static String TAG_DESCRIPTION = "description";
private static String TAG_CARD_THUMB = "card";
private static String TAG_BACKGROUND = "background";
private static String TAG_TITLE = "title";
private static HashMap<String, List<Movie>> sMovieList;
private static Context sContext;
private static String sPrefixUrl;
public static void setContext(Context context) {
if (sContext == null)
sContext = context;
}
public static HashMap<String, List<Movie>> getMovieList() {
return sMovieList;
}
public static HashMap<String, List<Movie>> buildMedia(Context ctx, String url)
throws JSONException {
if (null != sMovieList) {
return sMovieList;
}
sMovieList = new HashMap<String, List<Movie>>();
JSONObject jsonObj = new VideoProvider().parseUrl(url);
JSONArray categories = jsonObj.getJSONArray(TAG_GOOGLE_VIDEOS);
if (null != categories) {
Log.d(TAG, "category #: " + categories.length());
String title = new String();
String videoUrl = new String();
String bgImageUrl = new String();
String cardImageUrl = new String();
String studio = new String();
for (int i = 0; i < categories.length(); i++) {
JSONObject category = categories.getJSONObject(i);
String category_name = category.getString(TAG_CATEGORY);
JSONArray videos = category.getJSONArray(TAG_MEDIA);
Log.d(TAG,
"category: " + i + " Name:" + category_name + " video length: "
+ videos.length());
List<Movie> categoryList = new ArrayList<Movie>();
if (null != videos) {
for (int j = 0; j < videos.length(); j++) {
JSONObject video = videos.getJSONObject(j);
String description = video.getString(TAG_DESCRIPTION);
JSONArray videoUrls = video.getJSONArray(TAG_SOURCES);
if (null == videoUrls || videoUrls.length() == 0) {
continue;
}
title = video.getString(TAG_TITLE);
videoUrl = getVideoPrefix(category_name, videoUrls.getString(0));
bgImageUrl = getThumbPrefix(category_name, title,
video.getString(TAG_BACKGROUND));
cardImageUrl = getThumbPrefix(category_name, title,
video.getString(TAG_CARD_THUMB));
studio = video.getString(TAG_STUDIO);
categoryList.add(buildMovieInfo(category_name, title, description, studio,
videoUrl, cardImageUrl,
bgImageUrl));
}
sMovieList.put(category_name, categoryList);
}
}
}
return sMovieList;
}
private static Movie buildMovieInfo(String category,
String title,
String description,
String studio,
String videoUrl,
String cardImageUrl,
String bgImageUrl) {
Movie movie = new Movie();
movie.setId(Movie.getCount());
Movie.incrementCount();
movie.setTitle(title);
movie.setDescription(description);
movie.setStudio(studio);
movie.setCategory(category);
movie.setCardImageUrl(cardImageUrl);
movie.setBackgroundImageUrl(bgImageUrl);
movie.setVideoUrl(videoUrl);
return movie;
}
private static String getVideoPrefix(String category, String videoUrl) {
String ret = "";
ret = sPrefixUrl + category.replace(" ", "%20") + '/' +
videoUrl.replace(" ", "%20");
return ret;
}
private static String getThumbPrefix(String category, String title, String imageUrl) {
String ret = "";
ret = sPrefixUrl + category.replace(" ", "%20") + '/' +
title.replace(" ", "%20") + '/' +
imageUrl.replace(" ", "%20");
return ret;
}
protected JSONObject parseUrl(String urlString) {
Log.d(TAG, "Parse URL: " + urlString);
InputStream is = null;
sPrefixUrl = sContext.getResources().getString(R.string.prefix_url);
try {
java.net.URL url = new java.net.URL(urlString);
URLConnection urlConnection = url.openConnection();
is = new BufferedInputStream(urlConnection.getInputStream());
BufferedReader reader = new BufferedReader(new InputStreamReader(
urlConnection.getInputStream(), "iso-8859-1"), 8);
StringBuilder sb = new StringBuilder();
String line = null;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
String json = sb.toString();
return new JSONObject(json);
} catch (Exception e) {
Log.d(TAG, "Failed to parse the json for media list", e);
return null;
} finally {
if (null != is) {
try {
is.close();
} catch (IOException e) {
Log.d(TAG, "JSON feed closed", e);
}
}
}
}
}

View file

@ -0,0 +1,179 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.tvleanback.model;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
import java.net.URI;
import java.net.URISyntaxException;
/*
* Movie class represents video entity with title, description, image thumbs and video url.
*/
public class Movie implements Parcelable {
private static final String TAG = "Movie";
static final long serialVersionUID = 727566175075960653L;
private static int sCount = 0;
private String mId;
private String mTitle;
private String mDescription;
private String mBgImageUrl;
private String mCardImageUrl;
private String mVideoUrl;
private String mStudio;
private String mCategory;
public Movie() {
}
public Movie(Parcel in){
String[] data = new String[8];
in.readStringArray(data);
mId = data[0];
mTitle = data[1];
mDescription = data[2];
mBgImageUrl = data[3];
mCardImageUrl = data[4];
mVideoUrl = data[5];
mStudio = data[6];
mCategory = data[7];
}
public static String getCount() {
return Integer.toString(sCount);
}
public static void incrementCount() {
sCount++;
}
public String getId() {
return mId;
}
public void setId(String id) {
mId = id;
}
public String getTitle() {
return mTitle;
}
public void setTitle(String title) {
mTitle = title;
}
public String getDescription() {
return mDescription;
}
public void setDescription(String description) {
mDescription = description;
}
public String getStudio() {
return mStudio;
}
public void setStudio(String studio) {
mStudio = studio;
}
public String getVideoUrl() {
return mVideoUrl;
}
public void setVideoUrl(String videoUrl) {
mVideoUrl = videoUrl;
}
public String getBackgroundImageUrl() {
return mBgImageUrl;
}
public void setBackgroundImageUrl(String bgImageUrl) {
mBgImageUrl = bgImageUrl;
}
public String getCardImageUrl() {
return mCardImageUrl;
}
public void setCardImageUrl(String cardImageUrl) {
mCardImageUrl = cardImageUrl;
}
public String getCategory() {
return mCategory;
}
public void setCategory(String category) {
mCategory = category;
}
public URI getBackgroundImageURI() {
try {
return new URI(getBackgroundImageUrl());
} catch (URISyntaxException e) {
return null;
}
}
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeStringArray(new String[] {mId,
mTitle,
mDescription,
mBgImageUrl,
mCardImageUrl,
mVideoUrl,
mStudio,
mCategory});
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder(200);
sb.append("Movie{");
sb.append("mId=" + mId);
sb.append(", mTitle='" + mTitle + '\'');
sb.append(", mVideoUrl='" + mVideoUrl + '\'');
sb.append(", backgroundImageUrl='" + mBgImageUrl + '\'');
sb.append(", backgroundImageURI='" + getBackgroundImageURI().toString() + '\'');
sb.append(", mCardImageUrl='" + mCardImageUrl + '\'');
sb.append('}');
return sb.toString();
}
public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
public Movie createFromParcel(Parcel in) {
return new Movie(in);
}
public Movie[] newArray(int size) {
return new Movie[size];
}
};
}

View file

@ -0,0 +1,98 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.tvleanback.presenter;
import android.graphics.drawable.Drawable;
import android.support.v17.leanback.widget.ImageCardView;
import android.support.v17.leanback.widget.Presenter;
import android.util.Log;
import android.view.ViewGroup;
import com.bumptech.glide.Glide;
import com.example.android.tvleanback.R;
import com.example.android.tvleanback.model.Movie;
/*
* A CardPresenter is used to generate Views and bind Objects to them on demand.
* It contains an Image CardView
*/
public class CardPresenter extends Presenter {
private static final String TAG = "CardPresenter";
private static int CARD_WIDTH = 313;
private static int CARD_HEIGHT = 176;
private static int sSelectedBackgroundColor;
private static int sDefaultBackgroundColor;
private Drawable mDefaultCardImage;
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent) {
Log.d(TAG, "onCreateViewHolder");
sDefaultBackgroundColor = parent.getResources().getColor(R.color.default_background);
sSelectedBackgroundColor = parent.getResources().getColor(R.color.selected_background);
mDefaultCardImage = parent.getResources().getDrawable(R.drawable.movie);
ImageCardView cardView = new ImageCardView(parent.getContext()) {
@Override
public void setSelected(boolean selected) {
updateCardBackgroundColor(this, selected);
super.setSelected(selected);
}
};
cardView.setFocusable(true);
cardView.setFocusableInTouchMode(true);
updateCardBackgroundColor(cardView, false);
return new ViewHolder(cardView);
}
private static void updateCardBackgroundColor(ImageCardView view, boolean selected) {
int color = selected ? sSelectedBackgroundColor : sDefaultBackgroundColor;
// Both background colors should be set because the view's background is temporarily visible
// during animations.
view.setBackgroundColor(color);
view.findViewById(R.id.info_field).setBackgroundColor(color);
}
@Override
public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) {
Movie movie = (Movie) item;
ImageCardView cardView = (ImageCardView) viewHolder.view;
Log.d(TAG, "onBindViewHolder");
if (movie.getCardImageUrl() != null) {
cardView.setTitleText(movie.getTitle());
cardView.setContentText(movie.getStudio());
cardView.setMainImageDimensions(CARD_WIDTH, CARD_HEIGHT);
Glide.with(viewHolder.view.getContext())
.load(movie.getCardImageUrl())
.centerCrop()
.error(mDefaultCardImage)
.into(cardView.getMainImageView());
}
}
@Override
public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) {
Log.d(TAG, "onUnbindViewHolder");
ImageCardView cardView = (ImageCardView) viewHolder.view;
// Remove references to images so that the garbage collector can free up memory
cardView.setBadgeImage(null);
cardView.setMainImage(null);
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.tvleanback.presenter;
import android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter;
import com.example.android.tvleanback.model.Movie;
public class DetailsDescriptionPresenter extends AbstractDetailsDescriptionPresenter {
@Override
protected void onBindDescription(ViewHolder viewHolder, Object item) {
Movie movie = (Movie) item;
if (movie != null) {
viewHolder.getTitle().setText(movie.getTitle());
viewHolder.getSubtitle().setText(movie.getStudio());
viewHolder.getBody().setText(movie.getDescription());
}
}
}

View file

@ -0,0 +1,58 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.tvleanback.presenter;
import android.graphics.Color;
import android.support.v17.leanback.widget.Presenter;
import android.view.Gravity;
import android.view.ViewGroup;
import android.widget.TextView;
import com.example.android.tvleanback.R;
import com.example.android.tvleanback.ui.MainFragment;
public class GridItemPresenter extends Presenter {
private static int GRID_ITEM_WIDTH = 200;
private static int GRID_ITEM_HEIGHT = 200;
private MainFragment mainFragment;
public GridItemPresenter(MainFragment mainFragment) {
this.mainFragment = mainFragment;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent) {
TextView view = new TextView(parent.getContext());
view.setLayoutParams(new ViewGroup.LayoutParams(GRID_ITEM_WIDTH, GRID_ITEM_HEIGHT));
view.setFocusable(true);
view.setFocusableInTouchMode(true);
view.setBackgroundColor(mainFragment.getResources().getColor(R.color.default_background));
view.setTextColor(Color.WHITE);
view.setGravity(Gravity.CENTER);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ViewHolder viewHolder, Object item) {
((TextView) viewHolder.view).setText((String) item);
}
@Override
public void onUnbindViewHolder(ViewHolder viewHolder) {
}
}

View file

@ -0,0 +1,54 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.tvleanback.recommendation;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
/*
* This class extends BroadCastReceiver and publishes recommendations on bootup
*/
public class BootupActivity extends BroadcastReceiver {
private static final String TAG = "BootupActivity";
private static final long INITIAL_DELAY = 5000;
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "BootupActivity initiated");
if (intent.getAction().endsWith(Intent.ACTION_BOOT_COMPLETED)) {
scheduleRecommendationUpdate(context);
}
}
private void scheduleRecommendationUpdate(Context context) {
Log.d(TAG, "Scheduling recommendations update");
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Intent recommendationIntent = new Intent(context, UpdateRecommendationsService.class);
PendingIntent alarmIntent = PendingIntent.getService(context, 0, recommendationIntent, 0);
alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
INITIAL_DELAY,
AlarmManager.INTERVAL_HALF_HOUR,
alarmIntent);
}
}

View file

@ -0,0 +1,232 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.tvleanback.recommendation;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import com.example.android.tvleanback.R;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
/*
* This class builds recommendations as notifications with videos as inputs.
*/
public class RecommendationBuilder {
private static final String TAG = "RecommendationBuilder";
private static final String
BACKGROUND_URI_PREFIX = "content://com.example.android.tvleanback.recommendation/";
private Context mContext;
private int mId;
private int mPriority;
private int mSmallIcon;
private String mTitle;
private String mDescription;
private Bitmap mBitmap;
private String mBackgroundUri;
private String mGroupKey;
private String mSort;
private PendingIntent mIntent;
public RecommendationBuilder() {
}
public RecommendationBuilder setContext(Context context) {
mContext = context;
return this;
}
public RecommendationBuilder setId(int id) {
mId = id;
return this;
}
public RecommendationBuilder setPriority(int priority) {
mPriority = priority;
return this;
}
public RecommendationBuilder setTitle(String title) {
mTitle = title;
return this;
}
public RecommendationBuilder setDescription(String description) {
mDescription = description;
return this;
}
public RecommendationBuilder setBitmap(Bitmap bitmap) {
mBitmap = bitmap;
return this;
}
public RecommendationBuilder setBackground(String uri) {
mBackgroundUri = uri;
return this;
}
public RecommendationBuilder setIntent(PendingIntent intent) {
mIntent = intent;
return this;
}
public RecommendationBuilder setSmallIcon(int resourceId) {
mSmallIcon = resourceId;
return this;
}
public Notification build() {
Bundle extras = new Bundle();
File bitmapFile = getNotificationBackground(mContext, mId);
if (mBackgroundUri != null) {
extras.putString(Notification.EXTRA_BACKGROUND_IMAGE_URI,
Uri.parse(BACKGROUND_URI_PREFIX + Integer.toString(mId)).toString());
}
// the following simulates group assignment into "Top", "Middle", "Bottom"
// by checking mId and similarly sort order
mGroupKey = (mId < 3) ? "Top" : (mId < 5) ? "Middle" : "Bottom";
mSort = (mId < 3) ? "1.0" : (mId < 5) ? "0.7" : "0.3";
// save bitmap into files for content provider to serve later
try {
bitmapFile.createNewFile();
FileOutputStream fOut = new FileOutputStream(bitmapFile);
mBitmap.compress(Bitmap.CompressFormat.PNG, 85, fOut);
fOut.flush();
fOut.close();
} catch (IOException ioe) {
Log.d(TAG, "Exception caught writing bitmap to file!", ioe);
}
Notification notification = new NotificationCompat.BigPictureStyle(
new NotificationCompat.Builder(mContext)
.setAutoCancel(true)
.setContentTitle(mTitle)
.setContentText(mDescription)
.setPriority(mPriority)
.setLocalOnly(true)
.setOngoing(true)
/*
groupKey (optional): Can be used to group together recommendations, so
they are ranked by the launcher as a separate group. Can be useful if the
application has different sources for recommendations, like "trending",
"subscriptions", and "new music" categories for YouTube, where the user can
be more interested in recommendations from one group than another.
*/
.setGroup(mGroupKey)
/*
sortKey (optional): A float number between 0.0 and 1.0, used to indicate
the relative importance (and sort order) of a single recommendation within
its specified group. The recommendations will be ordered in decreasing
order of importance within a given group.
*/
.setSortKey(mSort)
.setColor(mContext.getResources().getColor(R.color.fastlane_background))
.setCategory(Notification.CATEGORY_RECOMMENDATION)
.setLargeIcon(mBitmap)
.setSmallIcon(mSmallIcon)
.setContentIntent(mIntent)
.setExtras(extras))
.build();
Log.d(TAG, "Building notification - " + this.toString());
return notification;
}
@Override
public String toString() {
return "RecommendationBuilder{" +
", mId=" + mId +
", mPriority=" + mPriority +
", mSmallIcon=" + mSmallIcon +
", mTitle='" + mTitle + '\'' +
", mDescription='" + mDescription + '\'' +
", mBitmap='" + mBitmap + '\'' +
", mBackgroundUri='" + mBackgroundUri + '\'' +
", mIntent=" + mIntent +
'}';
}
public static class RecommendationBackgroundContentProvider extends ContentProvider {
@Override
public boolean onCreate() {
return true;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}
@Override
public String getType(Uri uri) {
return null;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
return null;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
return null;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0;
}
@Override
/*
* content provider serving files that are saved locally when recommendations are built
*/
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
int backgroundId = Integer.parseInt(uri.getLastPathSegment());
File bitmapFile = getNotificationBackground(getContext(), backgroundId);
return ParcelFileDescriptor.open(bitmapFile, ParcelFileDescriptor.MODE_READ_ONLY);
}
}
private static File getNotificationBackground(Context context, int notificationId) {
return new File(context.getCacheDir(), "tmp" + Integer.toString(notificationId) + ".png");
}
}

View file

@ -0,0 +1,126 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.tvleanback.recommendation;
import android.app.IntentService;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.TaskStackBuilder;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.util.Log;
import com.bumptech.glide.Glide;
import com.example.android.tvleanback.R;
import com.example.android.tvleanback.data.VideoProvider;
import com.example.android.tvleanback.model.Movie;
import com.example.android.tvleanback.ui.MovieDetailsActivity;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
/*
* This class builds up to MAX_RECOMMMENDATIONS of recommendations and defines what happens
* when they're clicked from Recommendations section on Home screen
*/
public class UpdateRecommendationsService extends IntentService {
private static final String TAG = "RecommendationsService";
private static final int MAX_RECOMMENDATIONS = 3;
private static final int CARD_WIDTH = 313;
private static final int CARD_HEIGHT = 176;
private NotificationManager mNotificationManager;
public UpdateRecommendationsService() {
super(TAG);
}
@Override
protected void onHandleIntent(Intent intent) {
Log.d(TAG, "Updating recommendation cards");
HashMap<String, List<Movie>> recommendations = VideoProvider.getMovieList();
if (recommendations == null) {
return;
}
if (mNotificationManager == null) {
mNotificationManager = (NotificationManager) getApplicationContext()
.getSystemService(Context.NOTIFICATION_SERVICE);
}
RecommendationBuilder builder = new RecommendationBuilder()
.setContext(getApplicationContext())
.setSmallIcon(R.drawable.videos_by_google_icon);
// flatten to list
List flattenedRecommendations = new ArrayList();
for (Map.Entry<String, List<Movie>> entry : recommendations.entrySet()) {
for (Movie movie : entry.getValue()) {
Log.d(TAG, "Recommendation - " + movie.getTitle());
flattenedRecommendations.add(movie);
}
}
Collections.shuffle(flattenedRecommendations);
Movie movie;
for (int i = 0; i < flattenedRecommendations.size() && i < MAX_RECOMMENDATIONS; i++) {
movie = (Movie) flattenedRecommendations.get(i);
final RecommendationBuilder notificationBuilder = builder
.setBackground(movie.getCardImageUrl())
.setId(i+1)
.setPriority(MAX_RECOMMENDATIONS - i - 1)
.setTitle(movie.getTitle())
.setDescription(getString(R.string.popular_header))
.setIntent(buildPendingIntent(movie, i + 1));
try {
Bitmap bitmap = Glide.with(getApplicationContext())
.load(movie.getCardImageUrl())
.asBitmap()
.into(CARD_WIDTH, CARD_HEIGHT) // Only use for synchronous .get()
.get();
notificationBuilder.setBitmap(bitmap);
Notification notification = notificationBuilder.build();
mNotificationManager.notify(i + 1, notification);
} catch (InterruptedException | ExecutionException e) {
Log.e(TAG, "Could not create recommendation: " + e);
}
}
}
private PendingIntent buildPendingIntent(Movie movie, int id) {
Intent detailsIntent = new Intent(this, MovieDetailsActivity.class);
detailsIntent.putExtra(MovieDetailsActivity.MOVIE, movie);
detailsIntent.putExtra(MovieDetailsActivity.NOTIFICATION_ID, id);
TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
stackBuilder.addParentStack(MovieDetailsActivity.class);
stackBuilder.addNextIntent(detailsIntent);
// Ensure a unique PendingIntents, otherwise all recommendations end up with the same
// PendingIntent
detailsIntent.setAction(movie.getId());
return stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
}
}

View file

@ -0,0 +1,88 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.example.android.tvleanback.ui;
import android.app.Activity;
import android.app.Fragment;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ProgressBar;
import com.example.android.tvleanback.R;
/*
* BrowseErrorActivity shows how to use ErrorFragment
*/
public class BrowseErrorActivity extends Activity {
private static int TIMER_DELAY = 3000;
private static int SPINNER_WIDTH = 100;
private static int SPINNER_HEIGHT = 100;
private ErrorFragment mErrorFragment;
private SpinnerFragment mSpinnerFragment;
/**
* Called when the activity is first created.
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
testError();
}
private void testError() {
mErrorFragment = new ErrorFragment();
getFragmentManager().beginTransaction().add(R.id.main_frame, mErrorFragment).commit();
mSpinnerFragment = new SpinnerFragment();
getFragmentManager().beginTransaction().add(R.id.main_frame, mSpinnerFragment).commit();
final Handler handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
getFragmentManager().beginTransaction().remove(mSpinnerFragment).commit();
mErrorFragment.setErrorContent();
}
}, TIMER_DELAY);
}
@Override
public boolean onSearchRequested() {
startActivity(new Intent(this, SearchActivity.class));
return true;
}
static public class SpinnerFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
ProgressBar progressBar = new ProgressBar(container.getContext());
if (container instanceof FrameLayout) {
FrameLayout.LayoutParams layoutParams =
new FrameLayout.LayoutParams(SPINNER_WIDTH, SPINNER_HEIGHT, Gravity.CENTER);
progressBar.setLayoutParams(layoutParams);
}
return progressBar;
}
}
}

View file

@ -0,0 +1,49 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.example.android.tvleanback.ui;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import com.example.android.tvleanback.R;
/*
* This class demonstrates how to extend ErrorFragment
*/
public class ErrorFragment extends android.support.v17.leanback.app.ErrorFragment {
private static final String TAG = "ErrorFragment";
private static final boolean TRANSLUCENT = true;
@Override
public void onCreate(Bundle savedInstanceState) {
Log.d(TAG, "onCreate");
super.onCreate(savedInstanceState);
setTitle(getResources().getString(R.string.app_name));
}
void setErrorContent() {
setImageDrawable(getResources().getDrawable(R.drawable.lb_ic_sad_cloud));
setMessage(getResources().getString(R.string.error_fragment_message));
setDefaultBackground(TRANSLUCENT);
setButtonText(getResources().getString(R.string.dismiss_error));
setButtonClickListener(new View.OnClickListener() {
@Override
public void onClick(View arg0) {
getFragmentManager().beginTransaction().remove(ErrorFragment.this).commit();
}
});
}
}

View file

@ -0,0 +1,42 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.example.android.tvleanback.ui;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import com.example.android.tvleanback.R;
/*
* MainActivity class that loads MainFragment
*/
public class MainActivity extends Activity {
/**
* Called when the activity is first created.
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
}
@Override
public boolean onSearchRequested() {
startActivity(new Intent(this, SearchActivity.class));
return true;
}
}

View file

@ -0,0 +1,300 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.example.android.tvleanback.ui;
import android.app.LoaderManager;
import android.content.Intent;
import android.content.Loader;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
import android.support.v17.leanback.app.BackgroundManager;
import android.support.v17.leanback.app.BrowseFragment;
import android.support.v17.leanback.widget.ArrayObjectAdapter;
import android.support.v17.leanback.widget.HeaderItem;
import android.support.v17.leanback.widget.ImageCardView;
import android.support.v17.leanback.widget.ListRow;
import android.support.v17.leanback.widget.ListRowPresenter;
import android.support.v17.leanback.widget.OnItemViewClickedListener;
import android.support.v17.leanback.widget.OnItemViewSelectedListener;
import android.support.v17.leanback.widget.Presenter;
import android.support.v17.leanback.widget.Row;
import android.support.v17.leanback.widget.RowPresenter;
import android.support.v4.app.ActivityOptionsCompat;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.resource.drawable.GlideDrawable;
import com.bumptech.glide.request.animation.GlideAnimation;
import com.bumptech.glide.request.target.SimpleTarget;
import com.example.android.tvleanback.R;
import com.example.android.tvleanback.data.VideoItemLoader;
import com.example.android.tvleanback.data.VideoProvider;
import com.example.android.tvleanback.model.Movie;
import com.example.android.tvleanback.presenter.CardPresenter;
import com.example.android.tvleanback.presenter.GridItemPresenter;
import com.example.android.tvleanback.recommendation.UpdateRecommendationsService;
import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
/*
* Main class to show BrowseFragment with header and rows of videos
*/
public class MainFragment extends BrowseFragment implements
LoaderManager.LoaderCallbacks<HashMap<String, List<Movie>>> {
private static final String TAG = "MainFragment";
private static int BACKGROUND_UPDATE_DELAY = 300;
private static String mVideosUrl;
private final Handler mHandler = new Handler();
private ArrayObjectAdapter mRowsAdapter;
private Drawable mDefaultBackground;
private DisplayMetrics mMetrics;
private Timer mBackgroundTimer;
private URI mBackgroundURI;
private BackgroundManager mBackgroundManager;
@Override
public void onActivityCreated(Bundle savedInstanceState) {
Log.d(TAG, "onCreate");
super.onActivityCreated(savedInstanceState);
loadVideoData();
prepareBackgroundManager();
setupUIElements();
setupEventListeners();
}
@Override
public void onDestroy() {
super.onDestroy();
if (null != mBackgroundTimer) {
Log.d(TAG, "onDestroy: " + mBackgroundTimer.toString());
mBackgroundTimer.cancel();
}
}
private void prepareBackgroundManager() {
mBackgroundManager = BackgroundManager.getInstance(getActivity());
mBackgroundManager.attach(getActivity().getWindow());
mDefaultBackground = getResources().getDrawable(R.drawable.default_background);
mMetrics = new DisplayMetrics();
getActivity().getWindowManager().getDefaultDisplay().getMetrics(mMetrics);
}
private void setupUIElements() {
setBadgeDrawable(getActivity().getResources().getDrawable(R.drawable.videos_by_google_banner));
setTitle(getString(R.string.browse_title)); // Badge, when set, takes precedent over title
setHeadersState(HEADERS_ENABLED);
setHeadersTransitionOnBackEnabled(true);
// set fastLane (or headers) background color
setBrandColor(getResources().getColor(R.color.fastlane_background));
// set search icon color
setSearchAffordanceColor(getResources().getColor(R.color.search_opaque));
}
private void loadVideoData() {
VideoProvider.setContext(getActivity());
mVideosUrl = getActivity().getResources().getString(R.string.catalog_url);
getLoaderManager().initLoader(0, null, this);
}
private void setupEventListeners() {
setOnSearchClickedListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(getActivity(), SearchActivity.class);
startActivity(intent);
}
});
setOnItemViewClickedListener(new ItemViewClickedListener());
setOnItemViewSelectedListener(new ItemViewSelectedListener());
}
/*
* (non-Javadoc)
* @see android.support.v4.app.LoaderManager.LoaderCallbacks#onCreateLoader(int,
* android.os.Bundle)
*/
@Override
public Loader<HashMap<String, List<Movie>>> onCreateLoader(int arg0, Bundle arg1) {
Log.d(TAG, "VideoItemLoader created ");
return new VideoItemLoader(getActivity(), mVideosUrl);
}
/*
* (non-Javadoc)
* @see android.support.v4.app.LoaderManager.LoaderCallbacks#onLoadFinished(android
* .support.v4.content.Loader, java.lang.Object)
*/
@Override
public void onLoadFinished(Loader<HashMap<String, List<Movie>>> arg0,
HashMap<String, List<Movie>> data) {
mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
CardPresenter cardPresenter = new CardPresenter();
int i = 0;
for (Map.Entry<String, List<Movie>> entry : data.entrySet()) {
ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(cardPresenter);
List<Movie> list = entry.getValue();
for (int j = 0; j < list.size(); j++) {
listRowAdapter.add(list.get(j));
}
HeaderItem header = new HeaderItem(i, entry.getKey());
i++;
mRowsAdapter.add(new ListRow(header, listRowAdapter));
}
HeaderItem gridHeader = new HeaderItem(i, getString(R.string.more_samples));
GridItemPresenter gridPresenter = new GridItemPresenter(this);
ArrayObjectAdapter gridRowAdapter = new ArrayObjectAdapter(gridPresenter);
gridRowAdapter.add(getString(R.string.grid_view));
gridRowAdapter.add(getString(R.string.error_fragment));
gridRowAdapter.add(getString(R.string.personal_settings));
mRowsAdapter.add(new ListRow(gridHeader, gridRowAdapter));
setAdapter(mRowsAdapter);
updateRecommendations();
}
@Override
public void onLoaderReset(Loader<HashMap<String, List<Movie>>> arg0) {
mRowsAdapter.clear();
}
protected void setDefaultBackground(Drawable background) {
mDefaultBackground = background;
}
protected void setDefaultBackground(int resourceId) {
mDefaultBackground = getResources().getDrawable(resourceId);
}
protected void updateBackground(String uri) {
int width = mMetrics.widthPixels;
int height = mMetrics.heightPixels;
Glide.with(getActivity())
.load(uri)
.centerCrop()
.error(mDefaultBackground)
.into(new SimpleTarget<GlideDrawable>(width, height) {
@Override
public void onResourceReady(GlideDrawable resource,
GlideAnimation<? super GlideDrawable>
glideAnimation) {
mBackgroundManager.setDrawable(resource);
}
});
mBackgroundTimer.cancel();
}
protected void updateBackground(Drawable drawable) {
BackgroundManager.getInstance(getActivity()).setDrawable(drawable);
}
protected void clearBackground() {
BackgroundManager.getInstance(getActivity()).setDrawable(mDefaultBackground);
}
private void startBackgroundTimer() {
if (null != mBackgroundTimer) {
mBackgroundTimer.cancel();
}
mBackgroundTimer = new Timer();
mBackgroundTimer.schedule(new UpdateBackgroundTask(), BACKGROUND_UPDATE_DELAY);
}
private void updateRecommendations() {
Intent recommendationIntent = new Intent(getActivity(), UpdateRecommendationsService.class);
getActivity().startService(recommendationIntent);
}
private class UpdateBackgroundTask extends TimerTask {
@Override
public void run() {
mHandler.post(new Runnable() {
@Override
public void run() {
if (mBackgroundURI != null) {
updateBackground(mBackgroundURI.toString());
}
}
});
}
}
private final class ItemViewClickedListener implements OnItemViewClickedListener {
@Override
public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
RowPresenter.ViewHolder rowViewHolder, Row row) {
if (item instanceof Movie) {
Movie movie = (Movie) item;
Log.d(TAG, "Movie: " + movie.toString());
Intent intent = new Intent(getActivity(), MovieDetailsActivity.class);
intent.putExtra(MovieDetailsActivity.MOVIE, movie);
Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(
getActivity(),
((ImageCardView) itemViewHolder.view).getMainImageView(),
MovieDetailsActivity.SHARED_ELEMENT_NAME).toBundle();
getActivity().startActivity(intent, bundle);
} else if (item instanceof String) {
if (((String) item).indexOf(getString(R.string.grid_view)) >= 0) {
Intent intent = new Intent(getActivity(), VerticalGridActivity.class);
startActivity(intent);
} else if (((String) item).indexOf(getString(R.string.error_fragment)) >= 0) {
Intent intent = new Intent(getActivity(), BrowseErrorActivity.class);
startActivity(intent);
} else {
Toast.makeText(getActivity(), ((String) item), Toast.LENGTH_SHORT)
.show();
}
}
}
}
private final class ItemViewSelectedListener implements OnItemViewSelectedListener {
@Override
public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
RowPresenter.ViewHolder rowViewHolder, Row row) {
if (item instanceof Movie) {
mBackgroundURI = ((Movie) item).getBackgroundImageURI();
startBackgroundTimer();
}
}
}
}

View file

@ -0,0 +1,46 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.example.android.tvleanback.ui;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import com.example.android.tvleanback.R;
/*
* Details activity class that loads LeanbackDetailsFragment class
*/
public class MovieDetailsActivity extends Activity {
public static final String SHARED_ELEMENT_NAME = "hero";
public static final String MOVIE = "Movie";
public static final String NOTIFICATION_ID = "NotificationId";
/**
* Called when the activity is first created.
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.details);
}
@Override
public boolean onSearchRequested() {
startActivity(new Intent(this, SearchActivity.class));
return true;
}
}

View file

@ -0,0 +1,279 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.example.android.tvleanback.ui;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.support.v17.leanback.app.BackgroundManager;
import android.support.v17.leanback.widget.Action;
import android.support.v17.leanback.widget.ArrayObjectAdapter;
import android.support.v17.leanback.widget.ClassPresenterSelector;
import android.support.v17.leanback.widget.DetailsOverviewRow;
import android.support.v17.leanback.widget.DetailsOverviewRowPresenter;
import android.support.v17.leanback.widget.HeaderItem;
import android.support.v17.leanback.widget.ImageCardView;
import android.support.v17.leanback.widget.ListRow;
import android.support.v17.leanback.widget.ListRowPresenter;
import android.support.v17.leanback.widget.OnActionClickedListener;
import android.support.v17.leanback.widget.OnItemViewClickedListener;
import android.support.v17.leanback.widget.Presenter;
import android.support.v17.leanback.widget.Row;
import android.support.v17.leanback.widget.RowPresenter;
import android.support.v4.app.ActivityOptionsCompat;
import android.util.DisplayMetrics;
import android.util.Log;
import android.widget.Toast;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.resource.drawable.GlideDrawable;
import com.bumptech.glide.request.animation.GlideAnimation;
import com.bumptech.glide.request.target.SimpleTarget;
import com.example.android.tvleanback.R;
import com.example.android.tvleanback.Utils;
import com.example.android.tvleanback.data.VideoProvider;
import com.example.android.tvleanback.model.Movie;
import com.example.android.tvleanback.presenter.CardPresenter;
import com.example.android.tvleanback.presenter.DetailsDescriptionPresenter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/*
* LeanbackDetailsFragment extends DetailsFragment, a Wrapper fragment for leanback details screens.
* It shows a detailed view of video and its meta plus related videos.
*/
public class MovieDetailsFragment extends android.support.v17.leanback.app.DetailsFragment {
private static final String TAG = "DetailsFragment";
private static final int ACTION_WATCH_TRAILER = 1;
private static final int ACTION_RENT = 2;
private static final int ACTION_BUY = 3;
private static final int DETAIL_THUMB_WIDTH = 274;
private static final int DETAIL_THUMB_HEIGHT = 274;
private static final int NO_NOTIFICATION = -1;
private Movie mSelectedMovie;
private ArrayObjectAdapter mAdapter;
private ClassPresenterSelector mPresenterSelector;
private BackgroundManager mBackgroundManager;
private Drawable mDefaultBackground;
private DisplayMetrics mMetrics;
@Override
public void onCreate(Bundle savedInstanceState) {
Log.d(TAG, "onCreate DetailsFragment");
super.onCreate(savedInstanceState);
prepareBackgroundManager();
mSelectedMovie = (Movie) getActivity().getIntent()
.getParcelableExtra(MovieDetailsActivity.MOVIE);
if (mSelectedMovie != null || checkGlobalSearchIntent()) {
removeNotification(getActivity().getIntent()
.getIntExtra(MovieDetailsActivity.NOTIFICATION_ID, NO_NOTIFICATION));
setupAdapter();
setupDetailsOverviewRow();
setupDetailsOverviewRowPresenter();
setupMovieListRow();
setupMovieListRowPresenter();
updateBackground(mSelectedMovie.getBackgroundImageUrl());
setOnItemViewClickedListener(new ItemViewClickedListener());
} else {
Intent intent = new Intent(getActivity(), MainActivity.class);
startActivity(intent);
}
}
private void removeNotification(int notificationId) {
if (notificationId != NO_NOTIFICATION) {
NotificationManager notificationManager = (NotificationManager) getActivity()
.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(notificationId);
}
}
@Override
public void onStop() {
super.onStop();
}
/*
* Check if there is a global search intent
*/
private boolean checkGlobalSearchIntent() {
Intent intent = getActivity().getIntent();
String intentAction = intent.getAction();
String globalSearch = getString(R.string.global_search);
if (globalSearch.equalsIgnoreCase(intentAction)) {
Uri intentData = intent.getData();
Log.d(TAG, "action: " + intentAction + " intentData:" + intentData);
int selectedIndex = Integer.parseInt(intentData.getLastPathSegment());
HashMap<String, List<Movie>> movies = VideoProvider.getMovieList();
int movieTally = 0;
if (movies == null) {
return false;
}
for (Map.Entry<String, List<Movie>> entry : movies.entrySet()) {
List<Movie> list = entry.getValue();
for (Movie movie : list) {
movieTally++;
if (selectedIndex == movieTally) {
mSelectedMovie = movie;
return true;
}
}
}
}
return false;
}
private void prepareBackgroundManager() {
mBackgroundManager = BackgroundManager.getInstance(getActivity());
mBackgroundManager.attach(getActivity().getWindow());
mDefaultBackground = getResources().getDrawable(R.drawable.default_background);
mMetrics = new DisplayMetrics();
getActivity().getWindowManager().getDefaultDisplay().getMetrics(mMetrics);
}
protected void updateBackground(String uri) {
Glide.with(getActivity())
.load(uri)
.centerCrop()
.error(mDefaultBackground)
.into(new SimpleTarget<GlideDrawable>(mMetrics.widthPixels, mMetrics.heightPixels) {
@Override
public void onResourceReady(GlideDrawable resource, GlideAnimation<? super GlideDrawable> glideAnimation) {
mBackgroundManager.setDrawable(resource);
}
});
}
private void setupAdapter() {
mPresenterSelector = new ClassPresenterSelector();
mAdapter = new ArrayObjectAdapter(mPresenterSelector);
setAdapter(mAdapter);
}
private void setupDetailsOverviewRow() {
Log.d(TAG, "doInBackground: " + mSelectedMovie.toString());
final DetailsOverviewRow row = new DetailsOverviewRow(mSelectedMovie);
row.setImageDrawable(getResources().getDrawable(R.drawable.default_background));
int width = Utils.convertDpToPixel(getActivity()
.getApplicationContext(), DETAIL_THUMB_WIDTH);
int height = Utils.convertDpToPixel(getActivity()
.getApplicationContext(), DETAIL_THUMB_HEIGHT);
Glide.with(getActivity())
.load(mSelectedMovie.getCardImageUrl())
.centerCrop()
.error(R.drawable.default_background)
.into(new SimpleTarget<GlideDrawable>(width, height) {
@Override
public void onResourceReady(GlideDrawable resource,
GlideAnimation<? super GlideDrawable>
glideAnimation) {
Log.d(TAG, "details overview card image url ready: " + resource);
row.setImageDrawable(resource);
mAdapter.notifyArrayItemRangeChanged(0, mAdapter.size());
}
});
row.addAction(new Action(ACTION_WATCH_TRAILER, getResources().getString(
R.string.watch_trailer_1), getResources().getString(R.string.watch_trailer_2)));
row.addAction(new Action(ACTION_RENT, getResources().getString(R.string.rent_1),
getResources().getString(R.string.rent_2)));
row.addAction(new Action(ACTION_BUY, getResources().getString(R.string.buy_1),
getResources().getString(R.string.buy_2)));
mAdapter.add(row);
}
private void setupDetailsOverviewRowPresenter() {
// Set detail background and style.
DetailsOverviewRowPresenter detailsPresenter =
new DetailsOverviewRowPresenter(new DetailsDescriptionPresenter());
detailsPresenter.setBackgroundColor(getResources().getColor(R.color.selected_background));
detailsPresenter.setStyleLarge(true);
// Hook up transition element.
detailsPresenter.setSharedElementEnterTransition(getActivity(),
MovieDetailsActivity.SHARED_ELEMENT_NAME);
detailsPresenter.setOnActionClickedListener(new OnActionClickedListener() {
@Override
public void onActionClicked(Action action) {
if (action.getId() == ACTION_WATCH_TRAILER) {
Intent intent = new Intent(getActivity(), PlaybackOverlayActivity.class);
intent.putExtra(MovieDetailsActivity.MOVIE, mSelectedMovie);
startActivity(intent);
} else {
Toast.makeText(getActivity(), action.toString(), Toast.LENGTH_SHORT).show();
}
}
});
mPresenterSelector.addClassPresenter(DetailsOverviewRow.class, detailsPresenter);
}
private void setupMovieListRow() {
String subcategories[] = {getString(R.string.related_movies)};
HashMap<String, List<Movie>> movies = VideoProvider.getMovieList();
ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(new CardPresenter());
for (Map.Entry<String, List<Movie>> entry : movies.entrySet()) {
if (mSelectedMovie.getCategory().indexOf(entry.getKey()) >= 0) {
List<Movie> list = entry.getValue();
for (int j = 0; j < list.size(); j++) {
listRowAdapter.add(list.get(j));
}
}
}
HeaderItem header = new HeaderItem(0, subcategories[0]);
mAdapter.add(new ListRow(header, listRowAdapter));
}
private void setupMovieListRowPresenter() {
mPresenterSelector.addClassPresenter(ListRow.class, new ListRowPresenter());
}
private final class ItemViewClickedListener implements OnItemViewClickedListener {
@Override
public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
RowPresenter.ViewHolder rowViewHolder, Row row) {
if (item instanceof Movie) {
Movie movie = (Movie) item;
Log.d(TAG, "Item: " + item.toString());
Intent intent = new Intent(getActivity(), MovieDetailsActivity.class);
intent.putExtra(MovieDetailsActivity.MOVIE, movie);
Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(
getActivity(),
((ImageCardView) itemViewHolder.view).getMainImageView(),
MovieDetailsActivity.SHARED_ELEMENT_NAME).toBundle();
getActivity().startActivity(intent, bundle);
}
}
}
}

View file

@ -0,0 +1,282 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.example.android.tvleanback.ui;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.media.MediaMetadata;
import android.media.MediaPlayer;
import android.media.session.MediaSession;
import android.media.session.PlaybackState;
import android.net.Uri;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.util.Log;
import android.widget.FrameLayout;
import android.widget.VideoView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.animation.GlideAnimation;
import com.bumptech.glide.request.target.SimpleTarget;
import com.example.android.tvleanback.R;
import com.example.android.tvleanback.model.Movie;
/**
* PlaybackOverlayActivity for video playback that loads PlaybackOverlayFragment
*/
public class PlaybackOverlayActivity extends Activity implements
PlaybackOverlayFragment.OnPlayPauseClickedListener {
private static final String TAG = "PlaybackOverlayActivity";
private static final double MEDIA_HEIGHT = 0.95;
private static final double MEDIA_WIDTH = 0.95;
private static final double MEDIA_TOP_MARGIN = 0.025;
private static final double MEDIA_RIGHT_MARGIN = 0.025;
private static final double MEDIA_BOTTOM_MARGIN = 0.025;
private static final double MEDIA_LEFT_MARGIN = 0.025;
private VideoView mVideoView;
private LeanbackPlaybackState mPlaybackState = LeanbackPlaybackState.IDLE;
private MediaSession mSession;
/**
* Called when the activity is first created.
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.playback_controls);
loadViews();
//Example for handling resizing view for overscan
//overScan();
mSession = new MediaSession (this, "LeanbackSampleApp");
mSession.setCallback(new MediaSessionCallback());
mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS |
MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
mSession.setActive(true);
}
@Override
public void onDestroy() {
super.onDestroy();
mVideoView.suspend();
}
/**
* Implementation of OnPlayPauseClickedListener
*/
public void onFragmentPlayPause(Movie movie, int position, Boolean playPause) {
mVideoView.setVideoPath(movie.getVideoUrl());
if (position == 0 || mPlaybackState == LeanbackPlaybackState.IDLE) {
setupCallbacks();
mPlaybackState = LeanbackPlaybackState.IDLE;
}
if (playPause && mPlaybackState != LeanbackPlaybackState.PLAYING) {
mPlaybackState = LeanbackPlaybackState.PLAYING;
if (position > 0) {
mVideoView.seekTo(position);
mVideoView.start();
}
} else {
mPlaybackState = LeanbackPlaybackState.PAUSED;
mVideoView.pause();
}
updatePlaybackState(position);
updateMetadata(movie);
}
/**
* Implementation of OnPlayPauseClickedListener
*/
public void onFragmentFfwRwd(Movie movie, int position) {
mVideoView.setVideoPath(movie.getVideoUrl());
Log.d(TAG, "seek current time: " + position);
if (mPlaybackState == LeanbackPlaybackState.PLAYING) {
if (position > 0) {
mVideoView.seekTo(position);
mVideoView.start();
}
}
}
private void updatePlaybackState(int position) {
PlaybackState.Builder stateBuilder = new PlaybackState.Builder()
.setActions(getAvailableActions());
int state = PlaybackState.STATE_PLAYING;
if (mPlaybackState == LeanbackPlaybackState.PAUSED) {
state = PlaybackState.STATE_PAUSED;
}
stateBuilder.setState(state, position, 1.0f);
mSession.setPlaybackState(stateBuilder.build());
}
private long getAvailableActions() {
long actions = PlaybackState.ACTION_PLAY |
PlaybackState.ACTION_PLAY_FROM_MEDIA_ID |
PlaybackState.ACTION_PLAY_FROM_SEARCH;
if (mPlaybackState == LeanbackPlaybackState.PLAYING) {
actions |= PlaybackState.ACTION_PAUSE;
}
return actions;
}
private void updateMetadata(final Movie movie) {
final MediaMetadata.Builder metadataBuilder = new MediaMetadata.Builder();
String title = movie.getTitle().replace("_", " -");
metadataBuilder.putString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE, title);
metadataBuilder.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE,
movie.getDescription());
metadataBuilder.putString(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI,
movie.getCardImageUrl());
// And at minimum the title and artist for legacy support
metadataBuilder.putString(MediaMetadata.METADATA_KEY_TITLE, title);
metadataBuilder.putString(MediaMetadata.METADATA_KEY_ARTIST, movie.getStudio());
Glide.with(this)
.load(Uri.parse(movie.getCardImageUrl()))
.asBitmap()
.into(new SimpleTarget<Bitmap>(500, 500) {
@Override
public void onResourceReady(Bitmap bitmap, GlideAnimation anim) {
metadataBuilder.putBitmap(MediaMetadata.METADATA_KEY_ART, bitmap);
mSession.setMetadata(metadataBuilder.build());
}
});
}
private void loadViews() {
mVideoView = (VideoView) findViewById(R.id.videoView);
}
/**
* Example for handling resizing content for overscan. Typically you won't need to resize which
* is why overScan(); is commented out.
*/
private void overScan() {
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
int w = (int) (metrics.widthPixels * MEDIA_WIDTH);
int h = (int) (metrics.heightPixels * MEDIA_HEIGHT);
int marginLeft = (int) (metrics.widthPixels * MEDIA_LEFT_MARGIN);
int marginTop = (int) (metrics.heightPixels * MEDIA_TOP_MARGIN);
int marginRight = (int) (metrics.widthPixels * MEDIA_RIGHT_MARGIN);
int marginBottom = (int) (metrics.heightPixels * MEDIA_BOTTOM_MARGIN);
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(w, h);
lp.setMargins(marginLeft, marginTop, marginRight, marginBottom);
mVideoView.setLayoutParams(lp);
}
private void setupCallbacks() {
mVideoView.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
String msg = "";
if (extra == MediaPlayer.MEDIA_ERROR_TIMED_OUT) {
msg = getString(R.string.video_error_media_load_timeout);
} else if (what == MediaPlayer.MEDIA_ERROR_SERVER_DIED) {
msg = getString(R.string.video_error_server_inaccessible);
} else {
msg = getString(R.string.video_error_unknown_error);
}
mVideoView.stopPlayback();
mPlaybackState = LeanbackPlaybackState.IDLE;
return false;
}
});
mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
if (mPlaybackState == LeanbackPlaybackState.PLAYING) {
mVideoView.start();
}
}
});
mVideoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
mPlaybackState = LeanbackPlaybackState.IDLE;
}
});
}
@Override
public void onResume() {
super.onResume();
}
@Override
public void onPause() {
super.onPause();
if (mVideoView.isPlaying()) {
if (!requestVisibleBehind(true)) {
// Try to play behind launcher, but if it fails, stop playback.
stopPlayback();
}
} else {
requestVisibleBehind(false);
}
}
@Override
protected void onStop() {
super.onStop();
mSession.release();
}
@Override
public void onVisibleBehindCanceled() {
super.onVisibleBehindCanceled();
stopPlayback();
}
private void stopPlayback() {
if (mVideoView != null) {
mVideoView.stopPlayback();
}
}
@Override
public boolean onSearchRequested() {
startActivity(new Intent(this, SearchActivity.class));
return true;
}
/*
* List of various states that we can be in
*/
public static enum LeanbackPlaybackState {
PLAYING, PAUSED, BUFFERING, IDLE;
}
private class MediaSessionCallback extends MediaSession.Callback {
}
}

View file

@ -0,0 +1,502 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.example.android.tvleanback.ui;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.media.MediaMetadataRetriever;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter;
import android.support.v17.leanback.widget.Action;
import android.support.v17.leanback.widget.ArrayObjectAdapter;
import android.support.v17.leanback.widget.ClassPresenterSelector;
import android.support.v17.leanback.widget.ControlButtonPresenterSelector;
import android.support.v17.leanback.widget.HeaderItem;
import android.support.v17.leanback.widget.ImageCardView;
import android.support.v17.leanback.widget.ListRow;
import android.support.v17.leanback.widget.ListRowPresenter;
import android.support.v17.leanback.widget.OnActionClickedListener;
import android.support.v17.leanback.widget.OnItemViewClickedListener;
import android.support.v17.leanback.widget.OnItemViewSelectedListener;
import android.support.v17.leanback.widget.PlaybackControlsRow;
import android.support.v17.leanback.widget.PlaybackControlsRow.FastForwardAction;
import android.support.v17.leanback.widget.PlaybackControlsRow.PlayPauseAction;
import android.support.v17.leanback.widget.PlaybackControlsRow.RepeatAction;
import android.support.v17.leanback.widget.PlaybackControlsRow.RewindAction;
import android.support.v17.leanback.widget.PlaybackControlsRow.ShuffleAction;
import android.support.v17.leanback.widget.PlaybackControlsRow.SkipNextAction;
import android.support.v17.leanback.widget.PlaybackControlsRow.SkipPreviousAction;
import android.support.v17.leanback.widget.PlaybackControlsRow.ThumbsDownAction;
import android.support.v17.leanback.widget.PlaybackControlsRow.ThumbsUpAction;
import android.support.v17.leanback.widget.PlaybackControlsRowPresenter;
import android.support.v17.leanback.widget.Presenter;
import android.support.v17.leanback.widget.Row;
import android.support.v17.leanback.widget.RowPresenter;
import android.support.v4.app.ActivityOptionsCompat;
import android.util.Log;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.resource.drawable.GlideDrawable;
import com.bumptech.glide.request.animation.GlideAnimation;
import com.bumptech.glide.request.target.SimpleTarget;
import com.example.android.tvleanback.R;
import com.example.android.tvleanback.data.VideoProvider;
import com.example.android.tvleanback.model.Movie;
import com.example.android.tvleanback.presenter.CardPresenter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
/*
* Class for video playback with media control
*/
public class PlaybackOverlayFragment extends android.support.v17.leanback.app.PlaybackOverlayFragment {
private static final String TAG = "PlaybackOverlayFragment";
private static final boolean SHOW_DETAIL = true;
private static final boolean HIDE_MORE_ACTIONS = false;
private static final int PRIMARY_CONTROLS = 5;
private static final boolean SHOW_IMAGE = PRIMARY_CONTROLS <= 5;
private static final int BACKGROUND_TYPE = PlaybackOverlayFragment.BG_LIGHT;
private static final int CARD_WIDTH = 150;
private static final int CARD_HEIGHT = 240;
private static final int DEFAULT_UPDATE_PERIOD = 1000;
private static final int UPDATE_PERIOD = 16;
private static final int SIMULATED_BUFFERED_TIME = 10000;
private static final int CLICK_TRACKING_DELAY = 1000;
private static final int INITIAL_SPEED = 10000;
private static Context sContext;
private final Handler mClickTrackingHandler = new Handler();
OnPlayPauseClickedListener mCallback;
private ArrayObjectAdapter mRowsAdapter;
private ArrayObjectAdapter mPrimaryActionsAdapter;
private ArrayObjectAdapter mSecondaryActionsAdapter;
private PlayPauseAction mPlayPauseAction;
private RepeatAction mRepeatAction;
private ThumbsUpAction mThumbsUpAction;
private ThumbsDownAction mThumbsDownAction;
private ShuffleAction mShuffleAction;
private FastForwardAction mFastForwardAction;
private RewindAction mRewindAction;
private SkipNextAction mSkipNextAction;
private SkipPreviousAction mSkipPreviousAction;
private PlaybackControlsRow mPlaybackControlsRow;
private ArrayList<Movie> mItems = new ArrayList<Movie>();
private int mCurrentItem;
private long mDuration;
private Handler mHandler;
private Runnable mRunnable;
private Movie mSelectedMovie;
private int mFfwRwdSpeed = INITIAL_SPEED;
private Timer mClickTrackingTimer;
private int mClickCount;
@Override
public void onCreate(Bundle savedInstanceState) {
Log.i(TAG, "onCreate");
super.onCreate(savedInstanceState);
sContext = getActivity();
mItems = new ArrayList<Movie>();
mSelectedMovie = (Movie) getActivity()
.getIntent().getParcelableExtra(MovieDetailsActivity.MOVIE);
HashMap<String, List<Movie>> movies = VideoProvider.getMovieList();
if(movies != null) {
for (Map.Entry<String, List<Movie>> entry : movies.entrySet()) {
if (mSelectedMovie.getCategory().contains(entry.getKey())) {
List<Movie> list = entry.getValue();
if(list != null && !list.isEmpty()) {
for (int j = 0; j < list.size(); j++) {
mItems.add(list.get(j));
if (mSelectedMovie.getTitle().contentEquals(list.get(j).getTitle())) {
mCurrentItem = j;
}
}
}
}
}
}
mHandler = new Handler();
setBackgroundType(BACKGROUND_TYPE);
setFadingEnabled(false);
setupRows();
setOnItemViewSelectedListener(new OnItemViewSelectedListener() {
@Override
public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
RowPresenter.ViewHolder rowViewHolder, Row row) {
Log.i(TAG, "onItemSelected: " + item + " row " + row);
}
});
setOnItemViewClickedListener(new ItemViewClickedListener());
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
// This makes sure that the container activity has implemented
// the callback interface. If not, it throws an exception
try {
mCallback = (OnPlayPauseClickedListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement OnPlayPauseClickedListener");
}
}
@Override
public void onResume(){
super.onResume();
}
private void setupRows() {
ClassPresenterSelector ps = new ClassPresenterSelector();
PlaybackControlsRowPresenter playbackControlsRowPresenter;
if (SHOW_DETAIL) {
playbackControlsRowPresenter = new PlaybackControlsRowPresenter(
new DescriptionPresenter());
} else {
playbackControlsRowPresenter = new PlaybackControlsRowPresenter();
}
playbackControlsRowPresenter.setOnActionClickedListener(new OnActionClickedListener() {
public void onActionClicked(Action action) {
if (action.getId() == mPlayPauseAction.getId()) {
if (mPlayPauseAction.getIndex() == PlayPauseAction.PLAY) {
startProgressAutomation();
setFadingEnabled(true);
mCallback.onFragmentPlayPause(mItems.get(mCurrentItem),
mPlaybackControlsRow.getCurrentTime(), true);
} else {
stopProgressAutomation();
setFadingEnabled(false);
mCallback.onFragmentPlayPause(mItems.get(mCurrentItem),
mPlaybackControlsRow.getCurrentTime(), false);
}
} else if (action.getId() == mSkipNextAction.getId()) {
next();
} else if (action.getId() == mSkipPreviousAction.getId()) {
prev();
} else if (action.getId() == mFastForwardAction.getId()) {
fastForward();
} else if (action.getId() == mRewindAction.getId()) {
fastRewind();
}
if (action instanceof PlaybackControlsRow.MultiAction) {
((PlaybackControlsRow.MultiAction) action).nextIndex();
notifyChanged(action);
}
}
});
playbackControlsRowPresenter.setSecondaryActionsHidden(HIDE_MORE_ACTIONS);
ps.addClassPresenter(PlaybackControlsRow.class, playbackControlsRowPresenter);
ps.addClassPresenter(ListRow.class, new ListRowPresenter());
mRowsAdapter = new ArrayObjectAdapter(ps);
addPlaybackControlsRow();
addOtherRows();
setAdapter(mRowsAdapter);
}
private int getDuration() {
Movie movie = mItems.get(mCurrentItem);
MediaMetadataRetriever mmr = new MediaMetadataRetriever();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
mmr.setDataSource(movie.getVideoUrl(), new HashMap<String, String>());
} else {
mmr.setDataSource(movie.getVideoUrl());
}
String time = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
mDuration = Long.parseLong(time);
return (int) mDuration;
}
private void addPlaybackControlsRow() {
if (SHOW_DETAIL) {
mPlaybackControlsRow = new PlaybackControlsRow(mSelectedMovie);
} else {
mPlaybackControlsRow = new PlaybackControlsRow();
}
mRowsAdapter.add(mPlaybackControlsRow);
updatePlaybackRow(mCurrentItem);
ControlButtonPresenterSelector presenterSelector = new ControlButtonPresenterSelector();
mPrimaryActionsAdapter = new ArrayObjectAdapter(presenterSelector);
mSecondaryActionsAdapter = new ArrayObjectAdapter(presenterSelector);
mPlaybackControlsRow.setPrimaryActionsAdapter(mPrimaryActionsAdapter);
mPlaybackControlsRow.setSecondaryActionsAdapter(mSecondaryActionsAdapter);
mPlayPauseAction = new PlayPauseAction(sContext);
mRepeatAction = new RepeatAction(sContext);
mThumbsUpAction = new ThumbsUpAction(sContext);
mThumbsDownAction = new ThumbsDownAction(sContext);
mShuffleAction = new ShuffleAction(sContext);
mSkipNextAction = new PlaybackControlsRow.SkipNextAction(sContext);
mSkipPreviousAction = new PlaybackControlsRow.SkipPreviousAction(sContext);
mFastForwardAction = new PlaybackControlsRow.FastForwardAction(sContext);
mRewindAction = new PlaybackControlsRow.RewindAction(sContext);
if (PRIMARY_CONTROLS > 5) {
mPrimaryActionsAdapter.add(mThumbsUpAction);
} else {
mSecondaryActionsAdapter.add(mThumbsUpAction);
}
mPrimaryActionsAdapter.add(mSkipPreviousAction);
if (PRIMARY_CONTROLS > 3) {
mPrimaryActionsAdapter.add(new PlaybackControlsRow.RewindAction(sContext));
}
mPrimaryActionsAdapter.add(mPlayPauseAction);
if (PRIMARY_CONTROLS > 3) {
mPrimaryActionsAdapter.add(new PlaybackControlsRow.FastForwardAction(sContext));
}
mPrimaryActionsAdapter.add(mSkipNextAction);
mSecondaryActionsAdapter.add(mRepeatAction);
mSecondaryActionsAdapter.add(mShuffleAction);
if (PRIMARY_CONTROLS > 5) {
mPrimaryActionsAdapter.add(mThumbsDownAction);
} else {
mSecondaryActionsAdapter.add(mThumbsDownAction);
}
mSecondaryActionsAdapter.add(new PlaybackControlsRow.HighQualityAction(sContext));
mSecondaryActionsAdapter.add(new PlaybackControlsRow.ClosedCaptioningAction(sContext));
}
private void notifyChanged(Action action) {
ArrayObjectAdapter adapter = mPrimaryActionsAdapter;
if (adapter.indexOf(action) >= 0) {
adapter.notifyArrayItemRangeChanged(adapter.indexOf(action), 1);
return;
}
adapter = mSecondaryActionsAdapter;
if (adapter.indexOf(action) >= 0) {
adapter.notifyArrayItemRangeChanged(adapter.indexOf(action), 1);
return;
}
}
private void updatePlaybackRow(int index) {
if (mPlaybackControlsRow.getItem() != null) {
Movie item = (Movie) mPlaybackControlsRow.getItem();
item.setTitle(mItems.get(mCurrentItem).getTitle());
item.setStudio(mItems.get(mCurrentItem).getStudio());
}
if (SHOW_IMAGE) {
updateVideoImage(mItems.get(mCurrentItem).getCardImageUrl());
}
mRowsAdapter.notifyArrayItemRangeChanged(0, 1);
mPlaybackControlsRow.setTotalTime(getDuration());
mPlaybackControlsRow.setCurrentTime(0);
mPlaybackControlsRow.setBufferedProgress(0);
}
private void addOtherRows() {
ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(new CardPresenter());
for (Movie movie : mItems) {
listRowAdapter.add(movie);
}
HeaderItem header = new HeaderItem(0, getString(R.string.related_movies));
mRowsAdapter.add(new ListRow(header, listRowAdapter));
}
private int getUpdatePeriod() {
if (getView() == null || mPlaybackControlsRow.getTotalTime() <= 0) {
return DEFAULT_UPDATE_PERIOD;
}
return Math.max(UPDATE_PERIOD, mPlaybackControlsRow.getTotalTime() / getView().getWidth());
}
private void startProgressAutomation() {
mRunnable = new Runnable() {
@Override
public void run() {
int updatePeriod = getUpdatePeriod();
int currentTime = mPlaybackControlsRow.getCurrentTime() + updatePeriod;
int totalTime = mPlaybackControlsRow.getTotalTime();
mPlaybackControlsRow.setCurrentTime(currentTime);
mPlaybackControlsRow.setBufferedProgress(currentTime + SIMULATED_BUFFERED_TIME);
if (totalTime > 0 && totalTime <= currentTime) {
next();
}
mHandler.postDelayed(this, updatePeriod);
}
};
mHandler.postDelayed(mRunnable, getUpdatePeriod());
}
private void next() {
if (++mCurrentItem >= mItems.size()) {
mCurrentItem = 0;
}
if (mPlayPauseAction.getIndex() == PlayPauseAction.PLAY) {
mCallback.onFragmentPlayPause(mItems.get(mCurrentItem), 0, false);
} else {
mCallback.onFragmentPlayPause(mItems.get(mCurrentItem), 0, true);
}
mFfwRwdSpeed = INITIAL_SPEED;
updatePlaybackRow(mCurrentItem);
}
private void prev() {
if (--mCurrentItem < 0) {
mCurrentItem = mItems.size() - 1;
}
if (mPlayPauseAction.getIndex() == PlayPauseAction.PLAY) {
mCallback.onFragmentPlayPause(mItems.get(mCurrentItem), 0, false);
} else {
mCallback.onFragmentPlayPause(mItems.get(mCurrentItem), 0, true);
}
mFfwRwdSpeed = INITIAL_SPEED;
updatePlaybackRow(mCurrentItem);
}
private void fastForward() {
Log.d(TAG, "current time: " + mPlaybackControlsRow.getCurrentTime());
startClickTrackingTimer();
int currentTime = mPlaybackControlsRow.getCurrentTime() + mFfwRwdSpeed;
if (currentTime > (int) mDuration) {
currentTime = (int) mDuration;
}
fastFR(currentTime);
}
private void fastRewind() {
startClickTrackingTimer();
int currentTime = mPlaybackControlsRow.getCurrentTime() - mFfwRwdSpeed;
if (currentTime < 0 || currentTime > (int) mDuration) {
currentTime = 0;
}
fastFR(currentTime);
}
private void fastFR(int currentTime) {
mCallback.onFragmentFfwRwd(mItems.get(mCurrentItem), currentTime);
mPlaybackControlsRow.setCurrentTime(currentTime);
mPlaybackControlsRow.setBufferedProgress(currentTime + SIMULATED_BUFFERED_TIME);
}
private void stopProgressAutomation() {
if (mHandler != null && mRunnable != null) {
mHandler.removeCallbacks(mRunnable);
}
}
@Override
public void onStop() {
stopProgressAutomation();
super.onStop();
}
protected void updateVideoImage(String uri) {
Glide.with(sContext)
.load(uri)
.centerCrop()
.into(new SimpleTarget<GlideDrawable>(CARD_WIDTH, CARD_HEIGHT) {
@Override
public void onResourceReady(GlideDrawable resource, GlideAnimation<? super GlideDrawable> glideAnimation) {
mPlaybackControlsRow.setImageDrawable(resource);
mRowsAdapter.notifyArrayItemRangeChanged(0, mRowsAdapter.size());
}
});
}
private void startClickTrackingTimer() {
if (null != mClickTrackingTimer) {
mClickCount++;
mClickTrackingTimer.cancel();
} else {
mClickCount = 0;
mFfwRwdSpeed = INITIAL_SPEED;
}
mClickTrackingTimer = new Timer();
mClickTrackingTimer.schedule(new UpdateFfwRwdSpeedTask(), CLICK_TRACKING_DELAY);
}
// Container Activity must implement this interface
public interface OnPlayPauseClickedListener {
public void onFragmentPlayPause(Movie movie, int position, Boolean playPause);
public void onFragmentFfwRwd(Movie movie, int position);
}
static class DescriptionPresenter extends AbstractDetailsDescriptionPresenter {
@Override
protected void onBindDescription(ViewHolder viewHolder, Object item) {
viewHolder.getTitle().setText(((Movie) item).getTitle());
viewHolder.getSubtitle().setText(((Movie) item).getStudio());
}
}
private class UpdateFfwRwdSpeedTask extends TimerTask {
@Override
public void run() {
mClickTrackingHandler.post(new Runnable() {
@Override
public void run() {
if (mClickCount == 0) {
mFfwRwdSpeed = INITIAL_SPEED;
} else if (mClickCount == 1) {
mFfwRwdSpeed *= 2;
} else if (mClickCount >= 2) {
mFfwRwdSpeed *= 4;
}
mClickCount = 0;
mClickTrackingTimer = null;
}
});
}
}
private final class ItemViewClickedListener implements OnItemViewClickedListener {
@Override
public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
RowPresenter.ViewHolder rowViewHolder, Row row) {
if (item instanceof Movie) {
Movie movie = (Movie) item;
Log.d(TAG, "Item: " + item.toString());
Intent intent = new Intent(getActivity(), PlaybackOverlayActivity.class);
intent.putExtra(MovieDetailsActivity.MOVIE, movie);
Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(
getActivity(),
((ImageCardView) itemViewHolder.view).getMainImageView(),
MovieDetailsActivity.SHARED_ELEMENT_NAME).toBundle();
getActivity().startActivity(intent, bundle);
}
}
}
}

View file

@ -0,0 +1,75 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.example.android.tvleanback.ui;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.v17.leanback.widget.SpeechRecognitionCallback;
import android.util.Log;
import com.example.android.tvleanback.R;
/*
* SearchActivity for SearchFragment
*/
public class SearchActivity extends Activity {
/**
* Called when the activity is first created.
*/
private static final String TAG = "SearchActivity";
private static boolean DEBUG = true;
/**
* SpeechRecognitionCallback is not required and if not provided recognition will be handled
* using internal speech recognizer, in which case you must have RECORD_AUDIO permission
*/
private static final int REQUEST_SPEECH = 1;
private SearchFragment mFragment;
private SpeechRecognitionCallback mSpeechRecognitionCallback;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.search);
mFragment = (SearchFragment) getFragmentManager().findFragmentById(R.id.search_fragment);
mSpeechRecognitionCallback = new SpeechRecognitionCallback() {
@Override
public void recognizeSpeech() {
if (DEBUG) Log.v(TAG, "recognizeSpeech");
startActivityForResult(mFragment.getRecognizerIntent(), REQUEST_SPEECH);
}
};
mFragment.setSpeechRecognitionCallback(mSpeechRecognitionCallback);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (DEBUG) Log.v(TAG, "onActivityResult requestCode=" + requestCode +
" resultCode=" + resultCode +
" data=" + data);
if (requestCode == REQUEST_SPEECH && resultCode == RESULT_OK) {
mFragment.setSearchQuery(data, true);
}
}
@Override
public boolean onSearchRequested() {
startActivity(new Intent(this, SearchActivity.class));
return true;
}
}

View file

@ -0,0 +1,155 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.example.android.tvleanback.ui;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.support.v17.leanback.widget.ArrayObjectAdapter;
import android.support.v17.leanback.widget.HeaderItem;
import android.support.v17.leanback.widget.ImageCardView;
import android.support.v17.leanback.widget.ListRow;
import android.support.v17.leanback.widget.ListRowPresenter;
import android.support.v17.leanback.widget.ObjectAdapter;
import android.support.v17.leanback.widget.OnItemViewClickedListener;
import android.support.v17.leanback.widget.Presenter;
import android.support.v17.leanback.widget.Row;
import android.support.v17.leanback.widget.RowPresenter;
import android.support.v4.app.ActivityOptionsCompat;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
import com.example.android.tvleanback.R;
import com.example.android.tvleanback.data.VideoProvider;
import com.example.android.tvleanback.model.Movie;
import com.example.android.tvleanback.presenter.CardPresenter;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/*
* This class demonstrates how to do in-app search
*/
public class SearchFragment extends android.support.v17.leanback.app.SearchFragment
implements android.support.v17.leanback.app.SearchFragment.SearchResultProvider {
private static final String TAG = "SearchFragment";
private static final int SEARCH_DELAY_MS = 1000;
private ArrayObjectAdapter mRowsAdapter;
private Handler mHandler = new Handler();
private SearchRunnable mDelayedLoad;
private String mQuery;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
setSearchResultProvider(this);
setOnItemViewClickedListener(new ItemViewClickedListener());
mDelayedLoad = new SearchRunnable();
}
@Override
public ObjectAdapter getResultsAdapter() {
return mRowsAdapter;
}
@Override
public boolean onQueryTextChange(String newQuery) {
Log.i(TAG, String.format("Search Query Text Change %s", newQuery));
loadQuery(newQuery);
return true;
}
@Override
public boolean onQueryTextSubmit(String query) {
Log.i(TAG, String.format("Search Query Text Submit %s", query));
loadQuery(query);
return true;
}
private void loadQuery(String query) {
mQuery = query;
mRowsAdapter.clear();
mHandler.removeCallbacks(mDelayedLoad);
if (!TextUtils.isEmpty(query) && !query.equals("nil")) {
mDelayedLoad.setSearchQuery(query);
mHandler.postDelayed(mDelayedLoad, SEARCH_DELAY_MS);
}
}
private void loadRows(String query) {
HashMap<String, List<Movie>> movies = VideoProvider.getMovieList();
ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(new CardPresenter());
for (Map.Entry<String, List<Movie>> entry : movies.entrySet()) {
for (Movie movie : entry.getValue()) {
if (movie.getTitle().toLowerCase(Locale.ENGLISH)
.contains(query.toLowerCase(Locale.ENGLISH))
|| movie.getDescription().toLowerCase(Locale.ENGLISH)
.contains(query.toLowerCase(Locale.ENGLISH))) {
listRowAdapter.add(movie);
}
}
}
HeaderItem header = new HeaderItem(0, getResources().getString(R.string.search_results)
+ " '" + mQuery + "'");
mRowsAdapter.add(new ListRow(header, listRowAdapter));
}
private class SearchRunnable implements Runnable {
private volatile String searchQuery;
public SearchRunnable() {
}
public void run() {
loadRows(searchQuery);
}
public void setSearchQuery(String value) {
this.searchQuery = value;
}
}
private final class ItemViewClickedListener implements OnItemViewClickedListener {
@Override
public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
RowPresenter.ViewHolder rowViewHolder, Row row) {
if (item instanceof Movie) {
Movie movie = (Movie) item;
Log.d(TAG, "Movie: " + movie.toString());
Intent intent = new Intent(getActivity(), MovieDetailsActivity.class);
intent.putExtra(MovieDetailsActivity.MOVIE, movie);
Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(
getActivity(),
((ImageCardView) itemViewHolder.view).getMainImageView(),
MovieDetailsActivity.SHARED_ELEMENT_NAME).toBundle();
getActivity().startActivity(intent, bundle);
} else {
Toast.makeText(getActivity(), ((String) item), Toast.LENGTH_SHORT)
.show();
}
}
}
}

View file

@ -0,0 +1,42 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.example.android.tvleanback.ui;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import com.example.android.tvleanback.R;
/*
* VerticalGridActivity that loads VerticalGridFragment
*/
public class VerticalGridActivity extends Activity {
/**
* Called when the activity is first created.
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.vertical_grid);
getWindow().setBackgroundDrawableResource(R.drawable.grid_bg);
}
@Override
public boolean onSearchRequested() {
startActivity(new Intent(this, SearchActivity.class));
return true;
}
}

View file

@ -0,0 +1,123 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.example.android.tvleanback.ui;
import android.content.Intent;
import android.os.Bundle;
import android.support.v17.leanback.widget.ArrayObjectAdapter;
import android.support.v17.leanback.widget.ImageCardView;
import android.support.v17.leanback.widget.OnItemViewClickedListener;
import android.support.v17.leanback.widget.OnItemViewSelectedListener;
import android.support.v17.leanback.widget.Presenter;
import android.support.v17.leanback.widget.Row;
import android.support.v17.leanback.widget.RowPresenter;
import android.support.v17.leanback.widget.VerticalGridPresenter;
import android.support.v4.app.ActivityOptionsCompat;
import android.util.Log;
import android.view.View;
import com.example.android.tvleanback.R;
import com.example.android.tvleanback.data.VideoProvider;
import com.example.android.tvleanback.model.Movie;
import com.example.android.tvleanback.presenter.CardPresenter;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
/*
* VerticalGridFragment shows a grid of videos
*/
public class VerticalGridFragment extends android.support.v17.leanback.app.VerticalGridFragment {
private static final String TAG = "VerticalGridFragment";
private static final int NUM_COLUMNS = 5;
private ArrayObjectAdapter mAdapter;
@Override
public void onCreate(Bundle savedInstanceState) {
Log.d(TAG, "onCreate");
super.onCreate(savedInstanceState);
setTitle(getString(R.string.vertical_grid_title));
setupFragment();
}
private void setupFragment() {
VerticalGridPresenter gridPresenter = new VerticalGridPresenter();
gridPresenter.setNumberOfColumns(NUM_COLUMNS);
setGridPresenter(gridPresenter);
mAdapter = new ArrayObjectAdapter(new CardPresenter());
long seed = System.nanoTime();
HashMap<String, List<Movie>> movies = VideoProvider.getMovieList();
for (Map.Entry<String, List<Movie>> entry : movies.entrySet()) {
List<Movie> list = entry.getValue();
Collections.shuffle(list, new Random(seed));
for (Movie movie : list) {
mAdapter.add(movie);
}
}
setAdapter(mAdapter);
setOnSearchClickedListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(getActivity(), SearchActivity.class);
startActivity(intent);
}
});
setOnItemViewClickedListener(new ItemViewClickedListener());
setOnItemViewSelectedListener(new ItemViewSelectedListener());
}
private final class ItemViewClickedListener implements OnItemViewClickedListener {
@Override
public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
RowPresenter.ViewHolder rowViewHolder, Row row) {
if (item instanceof Movie) {
Movie movie = (Movie) item;
Log.d(TAG, "Item: " + item.toString());
Intent intent = new Intent(getActivity(), MovieDetailsActivity.class);
intent.putExtra(MovieDetailsActivity.MOVIE, movie);
Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(
getActivity(),
((ImageCardView) itemViewHolder.view).getMainImageView(),
MovieDetailsActivity.SHARED_ELEMENT_NAME).toBundle();
getActivity().startActivity(intent, bundle);
}
}
}
private final class ItemViewSelectedListener implements OnItemViewSelectedListener {
@Override
public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
RowPresenter.ViewHolder rowViewHolder, Row row) {
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,003 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 707 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 750 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:startColor="@color/background_gradient_start"
android:endColor="@color/background_gradient_end"
android:angle="-270" />
</shape>

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Some files were not shown because too many files have changed in this diff Show more