Beruflich Dokumente
Kultur Dokumente
Concepts
To start off let's learn a little bit about Android TV. What is Android TV and how is it different? At its
core, it is Android so most of the things that you've learned developing your mobile app can be
reused. The key difference is input and the presentation of information.
Android TV is designed for the 10 foot experience. Instead of using a touchscreen, users will be
navigating your app using a controller while sitting far away from the screen. Instead of swiping the
notification bar down, the notifications will be displayed as the top row of cards. And, the screen is
always filled with rich visual content.
In an effort to simplify integration for developers we created the Leanback library. Leanback has
extendable fragments to allow you to quickly and easily create rich animated experiences. The core
fragments we'll be working with are:
These fragments use the Model View Presenter pattern. You'll bind your data model to the view
using presenter classes.
If you don't have it installed yet, please download and install it.
The first thing we need to do is get the mobile app to build on. Open up your Terminal and run:
Open Android Studio and click File > Open from the menu bar or Open an Existing Android
Studio Project from the splash-screen and select the recently cloned folder.
Understanding the starter project
Each of the following checkpoints can be used as reference points to check your work or for help if
you encounter any issues. The checkpoint number corresponds with the codelab step - 1. (0 vs 1
indexing)
The application stores all its data in a ContentProvider backed up by a SQLite database. If you want to
learn more about querying databases take a look at ourdocumentation.
On the Android TV
Open Settings
Under preferences open Developer Options
If Developer Options doesn't display go into Device -> About, scroll down to Build and click
the build number a few times until you receive the "You're A Developer" toast. Then exit out
of Settings and open Settings up again. Click Debugging.
Change USB debugging to On
If you have a USB cable, connect your Android TV device directly to your machine. You should now
be ready to run the app. You can test if it worked by running the following on your development
machine:
adb devices
If your device is listed, you're now in business and can skip to running the app! If that doesn't work,
try the steps below.
Once you have the IP of the device, you can connect to it using adb connect in a terminal.
Next up
Let's start creating the video browsing experience.
Leanback dependencies
Android manifest update for Android TV
Extending the Leanback BrowseFragment
Leanback dependencies
Open the build.gradle (checkpoint_0) file.
build.gradle
compile 'com.android.support:appcompat-v7:23.3.0'
compile 'com.android.support:leanback-v17:23.3.0'
compile 'com.android.support:recyclerview-v7:23.3.0'
There are a few things to note here. The Leanback libraries that target API version 23 are
backwards compatible to API version 17. For apps that require support for previous versions of
Android, you should make sure that the code path utilizing libraries with higher minSdk does not run
on devices with version < minSdk (library).
Under fastlane right-click to create a new Blank Activity called LeanbackActivity, and click on Finish.
Once the class is created, delete the menu/menu_leanback.xml resource since it won't be used.
In theLeanbackActivity class, delete the onCreateOptionsMenu and onOptionsItemSelected functions.
Also set the LeanbackActivity class to extend Activity instead of ActionBarActivity.
First, declare that we want to use leanback. As a child of manifest add the following line:
AndroidManifest.xml
<uses-feature android:name="android.software.leanback"
android:required="false" />
AndroidManifest.xml
<activity
android:name=".fastlane.LeanbackActivity"
android:label="@string/title_activity_player"
android:theme="@style/AppTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
We are also adding a theme to the activity. In the next step we will create a values-television
directory and create values specific to television.
Since certain features are not available on TV, you need to define their requirement as optional. If
you use any of the following features you'll need to add android:required="false" to the definition.
Touchscreen android.hardware.touchscreen
Telephony android.hardware.telephony
Camera android.hardware.camera
GPS android.hardware.location.gps
Microphone android.hardware.microphone
<uses-feature android:name="android.hardware.touchscreen"
android:required="false" />
→ Right click on the res directory and create a new Android resource directory.
→ Create a new values resource directory add UI mode and select Television as the qualifier.
→ Right click on the res directory and create a new value resource file under the newly created
values-television. Name the resources file styles.xml.
→ Add the following styles which inherit from the Leanback theme to customize the look and feel.
</style>
We've included Leanback libraries, and now Android TV will launch into the correct activity. Let's
create the video browser.
Create a fragment that extends BrowseFragment
We'll leverage the Leanback BrowseFragment. The BrowseFragment class in the Leanback library
allows you to create a primary layout for browsing categories and rows of media items with a
minimum amount of code.
→ To the class, add a private member ArrayObjectAdapter. We'll get into details
about ArrayObjectAdapter in the next step.
→ We'll also add a helper function to initialize the fragment. In it we instantiate mRowsAdapter, set it
as the Adapter for the fragment then set our main color and badge which appears in the top right of
the browse view.
setBrandColor(ContextCompat.getColor(getContext(), R.color.primary));
setBadgeDrawable(ContextCompat.getDrawable(getContext(), R.drawable.filmi));
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
init();
}
Alright, onto the final step of this section, adding this fragment to the activity.
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main_frame"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:name="com.android.example.leanback.fastlane.LeanbackBrowseFragment"
android:id="@+id/browse_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
Congrats! The framework for the browse fragment is in place. The checkpoint_1 directory contains
all the changes described above in case you got stuck or want to compare notes.
Summary
In this step you've learned about:
Declaring dependencies
Updating the Android manifest
Extending the BrowseFragment
Next up
3. Loading content into the
Let's finish the browse fragment.
Concepts
First, let's cover how the BrowseFragment works. The BrowseFragment basically renders rows of data
that you provide.
Think of each row as two pieces, a HeaderItem which defines the category and an array of objects
represented by the ListRow class which defines the content.
The ArrayObjectAdapter is an array of the defined ListRows that aggregates the rows for
the BrowseFragment view.
We can store any sort of View in ListRows, but in our app we'll use the Leanback ImageCardView.
The zoom and additional detail effects are automatically handled by the Leanback library.
To tie your video data and the ImageCardView together, we use a Presenter. The Presenter defines
which elements of the view are populated from which elements of the model.
Lastly we have the ViewHolder which is a container for the created view.
Let's put all of these concepts together to create the video browsing experience.
Create a presenter
We need to create a presenter to tie our Video model to the ImageCardView.
→ Under fastlane create a new class called CardPresenter that extends Presenter.
→ Define class variables to store the desired ImageCardView height and width and the application
context.
@Override
public ViewHolder onCreateViewHolder(ViewGroup viewGroup) {
return null;
}
@Override
public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object o) {
}
@Override
public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) {
The base sample app already uses Picasso so this should be done for you. But if you want to add it
in a separate app, in your build.gradle file add the following dependency.
compile 'com.squareup.picasso:picasso:2.3.4'
@Override
public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
@Override
public void onBitmapFailed(Drawable errorDrawable) {
@Override
public void onPrepareLoad(Drawable placeHolderDrawable) {
}
}
→ Add a variable to store the ImageCardView we'll draw into once the bitmap is loaded.
→ Create a constructor with the target ImageCardView as the parameter and store it as the
instance's mImageCardView.
→ In onBitmapLoaded, create a new Drawable from the bitmap and set it as the main image for
the ImageCardView.
mImageCardView.setMainImage(errorDrawable);
→ As a child of CardPresenter, create an inner static class that extends Presenter.ViewHolder and
create the default constructor.
→ In the constructor, cast the view parameter as an ImageCardView and store it in mCardView.
Instantiate a new PicassoImageCardViewTarget passing the cardView as the target parameter.
Finally, get the default card image from resources.
Picasso.with(mContext)
.load(url)
.resize(CARD_WIDTH * 2, CARD_HEIGHT * 2)
.centerCrop()
.error(mDefaultCardImage)
.into(mImageCardViewTarget);
}
Now let's create the ImageCardView and bind it with some data from the model.
@Override
public ViewHolder onCreateViewHolder(ViewGroup viewGroup) {
We set the cardView Focusable and FocusableInTouchMode to true to enable it to be selected when
browsing through the rows of content. It's important to remember to set these fields to true when
implementing Android TV for your app.
@Override
public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object o) {
Video video = (Video) o;
((ViewHolder) viewHolder).mCardView.setTitleText(video.getTitle());
((ViewHolder) viewHolder).mCardView.setContentText(video.getDescription());
((ViewHolder) viewHolder).mCardView.setMainImageDimensions(CARD_WIDTH, CARD_HEIGHT);
((ViewHolder) viewHolder).updateCardViewImage(video.getThumbUrl());
}
import com.android.example.leanback.data.Video;
And our CardPresenter is complete. Let's fill out some ListRows with our video content.
In init after we've set the badge drawable, we'll loop through the categories and create a row of
content for each one.
In each row, we'll create an ObjectAdapter to define how to render the content that we'll pull from our
database. We'll load the videos, create a header, and finally instantiate a ListRow with the header
and video data and add it to mRowsAdapter.
setBrandColor(ContextCompat.getColor(getContext(), R.color.primary));
setBadgeDrawable(ContextCompat.getDrawable(getContext(), R.drawable.filmi));
In VideoDataManager set the LOADER_ID to a random integer and replace the video instantiation
with setting the mapper for mItemList.
Congrats! You've completed this step. Try running the app on Android TV.
You can modify the default Activity that's launched from Android Studio. Click Edit Configurations
Under Activity change the radio button to launch LeanbackActivity. You should see a screen similar to
the one below.
Summary
In this step you've learned about:
The BrowseFragment and how you can populate it with your videos
Tying your video data to the view through the MVP pattern
Next up
Let's create the video details activity.
DetailsFragment concepts
First let's cover some concepts about how the DetailsFragment works. It functions very similarly to
the BrowseFragment.
The Additional Row can be populated with a ListRow just like the BrowseFragment.
→ Just like when you created the LeanbackActivity, you'll need to delete the menu resources and
menu related methods and set it to extend Activity instead of ActionBarActivity.
→ Under layout, open activity_leanback_details. Replace the default layout with the following code.
Here, we're specifying that the Activity consists of a single fragment, VideoDetailsFragment.
Don't worry about the errors, we'll work on creating the Fragment in an upcoming step.
Now that we have the activity framework, let's add the Leanback style to the activity declaration in
the manifest.
<activity android:name=".fastlane.VideoDetailsActivity"
android:label="@string/title_activity_player"
android:theme="@style/AppTheme"
android:exported="true">
</activity>
Now that the activity is styled correctly, we need to create the VideoDetailsFragment that we
reference in the layout.
Create the VideoDetailsFragment framework
Under fastlane create a new class called VideoDetailsFragment extending DetailsFragment.
We'll define a few class variables to store the Video information and some constants for image sizes
and action ids.
Let's start by getting the selected video from the intent. We'll override onCreate and get the video
from the intent.
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
selectedVideo = (Video) getActivity()
.getIntent()
.getSerializableExtra(Video.INTENT_EXTRA_VIDEO);
}
Create DetailsDescriptionPresenter
Before we get the image and create the DetailsOverviewRow we need to define a Presenter to bind
the data. The Leanback framework provides the AbstractDetailsDescriptionPresenter class for this
purpose, a nearly complete implementation of the presenter for media item details.
→ Override onBindDescription. Cast the item as a Video object then get and set the Title, Subtitle and
Body.
@Override
protected void onBindDescription(ViewHolder viewHolder, Object o) {
Video video = (Video) o;
if (video != null) {
Log.d("Presenter", String.format("%s, %s, %s", video.getTitle(), video.getThumbUrl(),
video.getDescription()));
viewHolder.getTitle().setText(video.getTitle());
viewHolder.getSubtitle().setText(String.format(mContext.getString(R.string.rating), video.getRating()));
viewHolder.getBody().setText(video.getDescription());
}
}
return row;
}
}
Here we're instantiating a new DetailsOverviewRow passing in the current Video as the main item for
the details page. We create a holder Bitmap variable to load the thumbnail. We use Picasso to load
and resize the image and store in poster.
Finally, we specify the actions by creating a SparseArrayObjectAdapter to hold our actions, and
setting this as the DetailsOverviewRow's action adapter. In this app, we
have ACTION_PLAY and ACTION_WATCH_LATER, but you could define a purchase or rent action.
In strings.xml define the text for action_play and action_watch_later.
<string name="action_play">PLAY</string>
<string name="action_watch_later">WATCH LATER</string>
To assist in calculating the appropriate screen size in DP, create a utility function.
Now that the image has loaded, we can create the rest of the details fragment.
→ Override onPostExecute.
@Override
protected void onPostExecute(DetailsOverviewRow detailRow) {
→ Instantiate a new ClassPresenterSelector. This object allows you to define the presenters for each
portion of DetailFragment.
detailsPresenter.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.primary));
detailsPresenter.setInitialState(FullWidthDetailsOverviewRowPresenter.STATE_FULL);
detailsPresenter.setOnActionClickedListener(new OnActionClickedListener() {
@Override
public void onActionClicked(Action action) {
if (action.getId() == ACTION_PLAY) {
Intent intent = new Intent(getActivity(), PlayerActivity.class);
intent.putExtra(Video.INTENT_EXTRA_VIDEO, selectedVideo);
startActivity(intent);
} else {
Toast.makeText(getActivity(), action.toString(), Toast.LENGTH_SHORT).show();
}
}
});
ps.addClassPresenter(DetailsOverviewRow.class, detailsPresenter);
setAdapter(adapter);
When we're done, the onPostExecute method should look like the following:
@Override
protected void onPostExecute(DetailsOverviewRow detailRow) {
ClassPresenterSelector ps = new ClassPresenterSelector();
FullWidthDetailsOverviewRowPresenter detailsPresenter = new
FullWidthDetailsOverviewRowPresenter(new DetailsDescriptionPresenter(getContext()));
ps.addClassPresenter(DetailsOverviewRow.class, detailsPresenter);
String subcategories[] = {
"You may also like"
};
new DetailRowBuilderTask().execute(selectedVideo);
In init set the onItemViewClickedListener. Here we're using a helper function to generate
the onItemViewClickedListener.
As we now pass the Video object with an Intent, we use a key Video.INTENT_EXTRA_VIDEO for it.
Congrats, you've finished this step! Compile, run and watch how you can play videos now!
Summary
In this step you've learned about:
DetailsFragment
Details Row Presenters
How to add a recommendations row
Next up
Creating recommendations that display on the home screen.
Content recommendations appear as the first row of the TV launch screen after the first use of the
device. Contributing recommendations from your app's content catalog can help bring users back to
your app.
In this step, you'll learn how to create recommendations and provide them to the Android framework
so your app content can be easily discovered and enjoyed by users.
→ We'll define a few constants for rendering and tagging, and define a NotificationManager
public RecommendationsService() {
super("RecommendationsService");
}
→ Next override the onHandleIntent function. As an example recommendation service, we'll use the
same video selections as the browse fragment. We store a ContentProviderClient, then create
a Cursor from the client.
@Override
protected void onHandleIntent(Intent intent) {
ContentProviderClient client = getContentResolver()
.acquireContentProviderClient(VideoItemContract.VideoItem.buildDirUri());
try {
Cursor cursor = client.query(VideoItemContract.VideoItem.buildDirUri(),
VideoDataManager.PROJECTION,
null,
null,
VideoItemContract.VideoItem.DEFAULT_SORT);
→ Instantiate a NotificationManager.
int count = 1;
→ Loop through the cursor until we're out of recommendations or we've hit our max, and create
pending intents for each. The pending intents will direct the user to the details view of the video.
→ Finally close the cursor and catch potential errors and you should have something similar to the
code below.
@Override
protected void onHandleIntent(Intent intent) {
ContentProviderClient client =
getContentResolver().acquireContentProviderClient(VideoItemContract.VideoItem.buildDirUri());
try {
Cursor cursor = client.query(VideoItemContract.VideoItem.buildDirUri(),
VideoDataManager.PROJECTION, null, null, VideoItemContract.VideoItem.DEFAULT_SORT);
int count = 1;
while (cursor.moveToNext() && count <= MAX_RECOMMENDATIONS) {
Video video = mapper.bind(cursor);
PendingIntent pendingIntent = buildPendingIntent(video);
Bundle extras = new Bundle();
extras.putString(EXTRA_BACKGROUND_IMAGE_URL, video.getThumbUrl());
count++;
}
cursor.close();
} catch (RemoteException re) {
} finally {
mNotificationManager = null;
}
}
Building Recommendations
Once the recommended videos are loaded, the service must create recommendations and pass
them to the Android framework. The framework receives the recommendations as Notification
objects that use a specific template and are marked with a specific category.
The following code example demonstrates how to get an instance of the NotificationManager, build a
recommendation, and pass it to the manager. This code needs to be added in the while loop after
the PendingIntenthas been created.
public BootCompleteReceiver() {
}
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().endsWith(Intent.ACTION_BOOT_COMPLETED)) {
scheduleRecommendationUpdate(context);
}
}
private void scheduleRecommendationUpdate(Context context) {
alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
INITIAL_DELAY,
AlarmManager.INTERVAL_HALF_HOUR,
alarmIntent);
}
}
Important: Receiving a boot completed notification requires that your app requests
the RECEIVE_BOOT_COMPLETEDpermission. For more information,
see ACTION_BOOT_COMPLETED.
Try running it and you should start seeing recommendations after 30 minutes. If you want to see
something immediately, start the service through adb.
Summary
In this step you've learned about:
Next up
Adding polish animations and transitions.
Concepts
Using the BackgroundManager of the Leanback library.
Encapsulating background management logic in a single class BackgroundHelper
Using the Picasso library to load and manipulate bitmaps
Implementing a Picasso Target
Create BackgroundHelper
Under fastlane create a class BackgroundHelper which we are going to extend step by step to add
functionality to change the background image for our TV activities.
@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;
}
return mBackgroundManager.equals(that.mBackgroundManager);
}
@Override
public int hashCode() {
return mBackgroundManager.hashCode();
}
}
if (null != mBackgroundTimer) {
mBackgroundTimer.cancel();
}
}
@Override
public String key() {
return null;
}
}
RenderScript rs;
protected BlurTransform() {
// Exists only to defeat instantiation.
}
@Override
public Bitmap transform(Bitmap bitmap) {
// Create another bitmap that will hold the results of the filter.
Bitmap blurredBitmap = Bitmap.createBitmap(bitmap);
bitmap.recycle();
return blurredBitmap;
}
@Override
public String key() {
return "blur";
}
.transform(BlurTransform.getInstance(mActivity))
The BackgroundHelper should be updated each time the user selects an item view. Create a factory
method getDefaultSelectedListener which does that:
protected OnItemViewSelectedListener getDefaultItemSelectedListener() {
return new OnItemViewSelectedListener() {
public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
RowPresenter.ViewHolder rowViewHolder, Row row) {
if (item instanceof Video) {
bgHelper.setBackgroundUrl(((Video) item).getThumbUrl());
bgHelper.startBackgroundTimer();
}
}
};
}
setOnItemViewSelectedListener(getDefaultItemSelectedListener());
BackgroundHelper bgHelper;
We have some bonus content on leveraging the PlaybackOverlayFragment to easily add playback
controls. The example for adding a PlaybackOverlayFragment to an existing player and detecting
appropriate playback controls can be found in checkpoint_6. The PlaybackOverlayFragment also
allows you to help users find related content in your app without stopping playback.
If you're looking for sample code for additional features, check out our full sample. It includes sample
code for the search fragment, grid fragment and others.