AsyncTask is useful when you are working on UI (main) thread, and want to do some (maybe long) background process, and want to get callback after the background process finishes. For example, consider the case when you want to update UI of Activity according to the result of internet access. You must execute URLconnection in background thread, while updating UI must be done in UI thread after getting result. It can be implemented without caring about threads or handlers by using AsyncTask.
Basics
4 Steps method execution flow
onPreExecute: invoked on the UI thread, before doInBackground starts. Initial set up part.
doInBackground: invoked on the background thread. Long computation/process (ex. downloading) is executed in this method. It must be overriden.
onProgressUpdate: invoked on the UI thread, during doInBackground. It is invoked after doInBackground method calls publishProgress to update UI of background process progress.
onPostExecute: invoked on the UI thread, after doInBackground finishes. Final UI update part.
The purpose to use AsyncTask is usually updating UI according to the result of background task, so at least you usually implement 2. doInBackground (MUST) and 4. onPostExecute. (If you only need background process and callback to main thread is not necessary, just use Thread.start() or Handler.post() )
3 Types declaration
AsyncTask is used by extending the class to your custom Class. You need to specify 3 types when extending AsyncTask.
public class CustomAsyncTask extends AsyncTask<Params, Progress, Result> { ... }
Params: input parameter, used in doInBackground method.
Progress: the parameter passed from doInBackground to onProgressUpdate via publishProgress(Progress...).
Result: return parameter of doInBackground,which is passed to onPostExecute.
※ If not necessary, just set Void type for each.
Usage – template
AsyncTask side
Create subclass of AsyncTask. As written in Advance, it may be implemented as a inner class of Activity class.
import android.os.AsyncTask;
/**
* AsyncTask<Params, Progress, Result>
* Params: Input parameter type
* - arg of {@link #doInBackground}
*
* Progress: Progress parameter type
* - arg of {@link #onProgressUpdate}
*
* Result: Return parameter from background process
* - return type of {@link #doInBackground}
* - arg of {@link #onPostExecute}
*/
public class CustomAsyncTask extends AsyncTask<Void, Void, Void> {
private static final String TAG = CustomAsyncTask.class.getSimpleName();
private int mParam;
public CustomAsyncTask(int param) {
// Constructor can be used to set field variable.
mParam = param;
}
@Override
protected void onPreExecute() {
// Initial UI set up here (if necessary)
}
@Override
protected Void doInBackground(Void... params) {
// Implement (maybe long) background process here!
//publishProgress(); // If you want to update UI during this process
return null;
}
@Override
protected void onProgressUpdate(Void... values) {
// invoked when publishProgress() is called,
// to update UI of progress during doInBackground is running.
}
@Override
protected void onPostExecute(Void aVoid) {
// Finally, the result of doInBackground is handled on UI thread here.
}
}
Execution side
Usually the purpose is to update the UI, so execution is done from Activity/Fragment class.
int param;
...
new CustomAsyncTask(param).execute();
Note that even type of “params” is Void, the variables can be passed by Constructor. This is useful tip when you want to pass/input more than 2 types of variable.
AsyncTask is usually used with Activity/Fragment, while the background task may be completely independent process from Activity itself. So the question comes up: how to establish independency between Activity & AsyncTask? which is the next topic.
One of the biggest difference between Android phone & Android TV is their input method. Since TV does not support touchpad, and we shouldn’t expect users to use keyboard for TV, inputting words for TV is troublesome.
BrowseFragment contains a design layout for search function as well, and showing in-app icon on your application is very easy. Just implement setOnSearchClickedListener, that’s all.
// Existence of this method make In-app search icon visible
setOnSearchClickedListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
}
});
setSearchAffordanceColor method can be used for specifying search icon color.
// set search icon color
setSearchAffordanceColor(getResources().getColor(R.color.search_opaque));
SearchFragment
Search function need to be implemented to answer user’s search query. To show the search query input UI and search result UI, LeanbackLibrary provides SearchFragment. This time, when request search (by either voice search button or explicitly press search button icon), we invoke this SearchFragment from SearchActivity.
private void setupEventListeners() {
setOnItemViewSelectedListener(new ItemViewSelectedListener());
setOnItemViewClickedListener(new ItemViewClickedListener());
// Existence of this method make In-app search icon visible
setOnSearchClickedListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(getActivity(), SearchActivity.class);
startActivity(intent);
}
});
}
Start creating SearchActivity by right click on Package name, Create → New Activity → Blank Activity → type “SearchActivity”. After SearchActivity class and activity_search.xml layout have created, modify res/acitivity_search.xml as follows.
It attaches SearchFragment. Again, create SearchFragment by Create → New → Java class → SearchFragment, and make it a subclass of android.support.v17.leanback.app.SearchFragment.
If you build and run application here, application will crash because SearchFragment automatically start to get search query from internal speech recognizer.
If you do not supply a callback via setSpeechRecognitionCallback(SpeechRecognitionCallback), an internal speech recognizer will be used for which your application will need to request android.permission.RECORD_AUDIO.
So you need to do either
Implement setSpeechRecognitionCallback
Request android.permission.RECORD_AUDIO on AndroidManifest.xml
as follwing.
public class SearchFragment extends android.support.v17.leanback.app.SearchFragment {
private static final String TAG = SearchFragment.class.getSimpleName();
private static final int REQUEST_SPEECH = 0x00000010;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (!Utils.hasPermission(getActivity(), Manifest.permission.RECORD_AUDIO)) {
// 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
setSpeechRecognitionCallback(new SpeechRecognitionCallback() {
@Override
public void recognizeSpeech() {
Log.v(TAG, "recognizeSpeech");
try {
startActivityForResult(getRecognizerIntent(), REQUEST_SPEECH);
} catch (ActivityNotFoundException e) {
Log.e(TAG, "Cannot find activity for speech recognizer", e);
}
}
});
}
}
public static boolean hasPermission(final Context context, final String permission) {
return PackageManager.PERMISSION_GRANTED == context.getPackageManager().checkPermission(
permission, context.getPackageName());
}
Overriding onSearchRequeseted to activate in-app search
When user tries voice input search, onSearchRequested callback is executed and Google’s global contents search will be launched as default.
It is necessary to override this method if you want to activate in-app application search.
<Ref> It is written in the description of startSearch method that
It is typically called from onSearchRequested(), either directly from Activity.onSearchRequested() or from an overridden version in any given Activity. If your goal is simply to activate search, it is preferred to call onSearchRequested(), which may have been overridden elsewhere in your Activity. If your goal is to inject specific data such as context data, it is preferred to override onSearchRequested(), so that any callers to it will benefit from the override.
We override onSearchRequested method for both MainActivity & SearchActivity.
SearchResultProvider intereface is an intereface of Leanback library, used to listen search related event. We need to override 3 methods.
getResultsAdapter – returns the adapter which includes the search results, to show search results on SearchFragment.
onQueryTextChange – event listener which is called when user changes search query text.
onQueryTextSubmit – event listener which is called when user submitted search query text.
We need to register this SearchResultProvider by using setSearchResultProvider method, minimum implementation is like this,
public class SearchFragment extends android.support.v17.leanback.app.SearchFragment
implements android.support.v17.leanback.app.SearchFragment.SearchResultProvider {
private static final String TAG = SearchFragment.class.getSimpleName();
private static final int REQUEST_SPEECH = 0x00000010;
private ArrayObjectAdapter mRowsAdapter;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
setSearchResultProvider(this);
...
}
...
@Override
public ObjectAdapter getResultsAdapter() {
Log.d(TAG, "getResultsAdapter");
Log.d(TAG, mRowsAdapter.toString());
// It should return search result here,
// but static Movie Item list will be returned here now for practice.
ArrayList<Movie> mItems = MovieProvider.getMovieItems();
ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(new CardPresenter());
listRowAdapter.addAll(0, mItems);
HeaderItem header = new HeaderItem("Search results");
mRowsAdapter.add(new ListRow(header, listRowAdapter));
return mRowsAdapter;
}
@Override
public boolean onQueryTextChange(String newQuery){
Log.i(TAG, String.format("Search Query Text Change %s", newQuery));
return true;
}
@Override
public boolean onQueryTextSubmit(String query) {
Log.i(TAG, String.format("Search Query Text Submit %s", query));
return true;
}
Build and run
SearchActivity can be launched in 2 ways,
Explicitly click search icon of BrowseFragment.
When user starts voice input search from specific controller *1 (onSearchRequested will be called.)
*1 It is depending on the Android TV devices. For example, SONY BRAVIA provides touchpad remote controller and voice search can be done from this remote controller.
[Update on 1. 8. 2016]: I’m sorry that it seems the implementation is insufficient. We should implement OnItemViewClickedListenerto define the action of clicking these search results.ThisOnItemViewClickedListenercan be set it by
The answer is written in Content License page, which says that
For the purposes of licensing, the content of this web site is divided into two categories:
Documentation content, including both static documentation and content extracted from source code modules, as well as sample code, and
All other site content
The documentation content on this site is made available to you as part of the Android Open Source Project. This documentation, including any code shown in it, is licensed under the Apache 2.0 license, the preferred license for all parts of the of the Android Open Source Project.
Exact Reproductions
If your online work exactly reproduces text or images from this site, in whole or in part, please include a paragraph at the bottom of your page that reads:
Also, please link back to the original source page so that readers can refer there for more information.
Modified Versions
If your online work shows modified text or images based on the content from this site, please include a paragraph at the bottom of your page that reads:
Above works for jpg, png type drawables, but it does work for xml type of drawable (I guess because xml file has no specific “width”, “height” information).
In this case, you can use following util method.
public static Bitmap drawableToBitmap (Drawable drawable) {
Bitmap bitmap = null;
if (drawable instanceof BitmapDrawable) {
BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
if(bitmapDrawable.getBitmap() != null) {
return bitmapDrawable.getBitmap();
}
}
if(drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); // Single color bitmap will be created of 1x1 pixel
} else {
bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
}
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
}
Drawable d = new BitmapDrawable(getResources(), bitmap);
Bitmap to Drawable
private static final int IMAGE_WIDTH = 320;
private static final int IMAGE_HEIGHT = 180;
Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, IMAGE_WIDTH, IMAGE_HEIGHT, true);
A Drawable that wraps a bitmap and can be tiled, stretched, or aligned. You can create a BitmapDrawable from a file path, an input stream, through XML inflation, or from a Bitmap object.
Recently I updated Android studio to version 1.3, and then it fails gradle sync. Error message is something like,
Error:(30, 13) Failed to resolve: com.squareup.picasso:picasso:2.5.2
Show in File
Show in Project Structure dialog
It seems that https proxy server was not working correctly, even if I’m setting proxy server in [File] → [Settings…] → [Appearance & Behavior] → [HTTP Proxy].
Solution
You can set http proxy server & https proxy server manually in gradle.properties file. Add below to your gradle.properties (Project Properties) file.
Android TV’s home, LeanbackLauncher has a recommendation row at first row. Any application can suggest recommended contents for users. In this chapter, I will explain how to show recommendation card to LeanbackLauncher from your application.
This recommendation is achieved by using Notification framework whose structure was already existed in Android phone/tablet SDK. So showing recommendation in LeanbackLauncher is actually sending notification. Basic sequence is following
Declare NotificationManager
Use your customized RecommendationBuilder class (it is custom class which usesNotificationCompat class) to prepare recommendation.
Make Notification by building RecommendationBuilder
Notify this Notification using NotificationManager.
What is implemented in this chapter?
2 new class, RecommendationBuilder & RecommendationFactory, are implemented in this chapter. RecommendationBuilder is the custom class to create notification for your application. RecommendationFactory actually create notification using RecommendationBuilder.
In this chapter, we will send notification (make recommendation) by clicking the button in MainFragment. It will invoke recommend method in RecommendationFactory to achive recommendation. (As written later, recommendation usually should be done in service. However I implemented recommendation in onClick for easy understanding.)
RecommendationBuilder
First, RecommendationBuilder is implemented as follows.
package com.corochann.androidtvapptutorial;
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 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.class.getSimpleName();
private static final String BACKGROUND_URI_PREFIX = "content://com.corochann.androidtvapptutorial/";
private Context mContext;
private int mId;
private int mPriority;
private int mFastLaneColor;
private int mSmallIcon;
private String mTitle;
private String mDescription;
private Bitmap mCardImageBitmap;
private String mBackgroundUri;
private Bitmap mBackgroundBitmap;
private String mGroupKey;
private String mSort;
private PendingIntent mIntent;
public RecommendationBuilder(Context context) {
mContext = context;
// default fast lane color
setFastLaneColor(mContext.getResources().getColor(R.color.fastlane_background));
}
public RecommendationBuilder setFastLaneColor(int color) {
mFastLaneColor = color;
return this;
}
/* context must not be null. It should be specified in constructor */
/*
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) {
mCardImageBitmap = bitmap;
return this;
}
public RecommendationBuilder setBackground(String uri) {
mBackgroundUri = uri;
return this;
}
public RecommendationBuilder setBackground(Bitmap bitmap) {
mBackgroundBitmap = bitmap;
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 (mBackgroundBitmap != null) {
Log.d(TAG, "making URI for mBackgroundBitmap");
extras.putString(Notification.EXTRA_BACKGROUND_IMAGE_URI,
Uri.parse(BACKGROUND_URI_PREFIX + Integer.toString(mId)).toString());
} else {
Log.w(TAG, "mBackgroundBitmap is null");
}
// the following simulates group assignment into "Top", "Middle", "Bottom"
// by checking mId and similarly sort order
mGroupKey = (mId < 3) ? "Group1" : (mId < 5) ? "Group2" : "Group3";
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);
mBackgroundBitmap.compress(Bitmap.CompressFormat.PNG, 85, fOut); //
You can use this RecommendationBuilder class as a library. In that case you don’t need to know the detail of this implementation, so you can skip reading this section. Explanation about this class is written in the remaining of this section.
Most important part is build() function, which shows how to make Notification instance for Android TV recommendation. Recommendation in Android TV is indeed Notification! It is making a instance of Notification class, by using NotificationCompat.BigPictureStyle method.
Many of the function and its related part in the recommendation card is straightforward.
Most tricky part in this function is setting background image. The background image will be displayed when user selects the recommendation card in LeanbackLauncher. This background image is specified by “extra” field, by using Bundle. Key is Notification. EXTRA_BACKGROUND_IMAGE_URI and the value is the URI of backgroundimage. Please note that you can specify bitmap file itself for
smallicon – used as application/company logo, shown in the right bottom side of recommendation card.
largeicon – used as main image for recommendation card.
But you cannot specify the background image by bitmap. In this implementation, we cache bitmap background image by using content provider and specifying URI of this cached background image.
Storing part is as follows
public Notification build() {
Bundle extras = new Bundle();
File bitmapFile = getNotificationBackground(mContext, mId);
...
// save bitmap into files for content provider to serve later
try {
bitmapFile.createNewFile();
FileOutputStream fOut = new FileOutputStream(bitmapFile);
mBackgroundBitmap.compress(Bitmap.CompressFormat.PNG, 85, fOut); // <- background bitmap must be created by mBackgroundUri, and not mCardImageBitmap
fOut.flush();
fOut.close();
} catch (IOException ioe) {
Log.d(TAG, "Exception caught writing bitmap to file!", ioe);
}
The part of getting background image URI is as follows. When Recommendation card is selected, it will refer extra fields with the key “Notification.EXTRA_BACKGROUND_IMAGE_URI“. The value is sent to Content provider’s openFile method. So the values are handled in the openFile method to extract the file path of already stored background image.
public Notification build() {
Bundle extras = new Bundle();
File bitmapFile = getNotificationBackground(mContext, mId);
if (mBackgroundBitmap != null) {
Log.d(TAG, "making URI for mBackgroundBitmap");
extras.putString(Notification.EXTRA_BACKGROUND_IMAGE_URI,
Uri.parse(BACKGROUND_URI_PREFIX + Integer.toString(mId)).toString());
} else {
Log.w(TAG, "mBackgroundBitmap is null");
}
...
}
public static class RecommendationBackgroundContentProvider extends ContentProvider {
...
@Override
/*
* content provider serving files that are saved locally when recommendations are built
*/
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
Log.i(TAG, "openFile");
int backgroundId = Integer.parseInt(uri.getLastPathSegment());
File bitmapFile = getNotificationBackground(getContext(), backgroundId);
return ParcelFileDescriptor.open(bitmapFile, ParcelFileDescriptor.MODE_READ_ONLY);
}
}
RecommendationFactory
RecommendationFactory class uses the RecommendationBuilder to create notification for recommending Movie items. Implementation is following.
package com.corochann.androidtvapptutorial;
import android.app.Activity;
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.support.v4.app.NotificationCompat;
import android.util.Log;
import com.squareup.picasso.Picasso;
import java.net.URI;
public class RecommendationFactory {
private static final String TAG = RecommendationFactory.class.getSimpleName();
private static final int CARD_WIDTH = 500;
private static final int CARD_HEIGHT = 500;
private static final int BACKGROUND_WIDTH = 1920;
private static final int BACKGROUND_HEIGHT = 1080;
private Context mContext;
private NotificationManager mNotificationManager;
public RecommendationFactory(Context context) {
mContext = context;
if (mNotificationManager == null) {
mNotificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
}
}
public void recommend(int id, Movie movie) {
recommend(id, movie, NotificationCompat.PRIORITY_DEFAULT);
}
/**
* create a notification for recommending item of Movie class
* @param movie
*/
public void recommend(final int id, final Movie movie, final int priority) {
Log.i(TAG, "recommend");
/* Run in background thread, since bitmap loading must be done in background */
new Thread(new Runnable() {
@Override
public void run() {
Log.i(TAG, "recommendation in progress");
Bitmap backgroundBitmap = prepareBitmap(movie.getCardImageUrl(), BACKGROUND_WIDTH, BACKGROUND_HEIGHT);
Bitmap cardImageBitmap = prepareBitmap(movie.getCardImageUrl(), CARD_WIDTH, CARD_HEIGHT);
PendingIntent intent = buildPendingIntent(movie, id);
RecommendationBuilder recommendationBuilder = new RecommendationBuilder(mContext)
.setSmallIcon(R.mipmap.ic_launcher)
.setBackground(backgroundBitmap)
.setId(id)
.setPriority(priority)
.setTitle(movie.getTitle())
.setDescription(movie.getDescription())
.setIntent(intent)
.setBitmap(cardImageBitmap)
.setFastLaneColor(mContext.getResources().getColor(R.color.fastlane_background));
Notification recommendNotification = recommendationBuilder.build();
mNotificationManager.notify(id, recommendNotification);
}}).start();
}
/**
* prepare bitmap from URL string
* @param url
* @return
*/
public Bitmap prepareBitmap(String url, int width, int height) {
Bitmap bitmap = null;
try {
URI uri = new URI(url);
bitmap = Picasso.with(mContext)
.load(uri.toString())
.resize(width, height)
.get();
} catch (Exception e) {
Log.e(TAG, e.toString());
}
return bitmap;
}
private PendingIntent buildPendingIntent(Movie movie, int id) {
Intent detailsIntent = new Intent(mContext, DetailsActivity.class);
detailsIntent.putExtra(DetailsActivity.MOVIE, movie);
detailsIntent.putExtra(DetailsActivity.NOTIFICATION_ID, id);
TaskStackBuilder stackBuilder = TaskStackBuilder.create(mContext);
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()));
return stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
}
}
recommend method is the main method, which makes the recommend Notification. Procedure is following
Declare NotificationManager It is done in the Constructor of RecommendationFactory class.
public RecommendationFactory(Context context) {
mContext = context;
if (mNotificationManager == null) {
mNotificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
}
}
Use your customized RecommendationBuilder class (it is custom class which usesNotificationCompat class) to prepare recommendation. Brief explanation of each set function
Id – ID for this recommendation card.
Priority – set priority. high priority card is more likely to be shown in the recommendation row in LeanbackLauncher.
Background – set background bitmap, which will be shown when user select recommendation card.
Title – first row text of recommendation card.
Description – second row text of recommendation card.
Bitmap – main image of recommendation card.
SmallIcon – company/image icon.
FastLaneColor – set background color of text field in the recommendation card
Intent – set PendingIntent (action) to indicate that what will happen after user click this recommendation card. buildPendingIntent method is used to create intent in this example.
Make Notification by building RecommendationBuilder
Notify this Notification using NotificationManager. These are done in recommend method.
public void recommend(final int id, final Movie movie, final int priority) {
Log.i(TAG, "recommend");
/* Run in background thread, since bitmap loading must be done in background */
new Thread(new Runnable() {
@Override
public void run() {
Log.i(TAG, "recommendation in progress");
Bitmap backgroundBitmap = prepareBitmap(movie.getCardImageUrl(), BACKGROUND_WIDTH, BACKGROUND_HEIGHT);
Bitmap cardImageBitmap = prepareBitmap(movie.getCardImageUrl(), CARD_WIDTH, CARD_HEIGHT);
PendingIntent intent = buildPendingIntent(movie, id);
// 2 prepare recommendation
RecommendationBuilder recommendationBuilder = new RecommendationBuilder(mContext)
.setSmallIcon(R.mipmap.ic_launcher)
.setBackground(backgroundBitmap)
.setId(id)
.setPriority(priority)
.setTitle(movie.getTitle())
.setDescription(movie.getDescription())
.setIntent(intent)
.setBitmap(cardImageBitmap)
.setFastLaneColor(mContext.getResources().getColor(R.color.fastlane_background));
// 3 make notification
Notification recommendNotification = recommendationBuilder.build();
// 4 norify
mNotificationManager.notify(id, recommendNotification);
}}).start();
}
That’s all. It is easy.
Build & run
You can send recommendation card by clicking “recommendation” button.
Recommendation is a concept that application suggests user to navigate next action. So most of the time, recommendation may be done in the background. I will follow up about this in the remainder.
Official doc Recommending TV Content – Android developers explains how to do recommendation in the background service, and Google’s sample code is showing real source code implementation of usage of background service. In this sample source code, recommendation service will be launched after getting the intent of “BOOT_COMPLETED”, and it will execute recommendation in background every 30 min.
Now Android TV is working on Android platform. Some may be interested in CPU, memory, harddisk information about your TV. We can check these CPU, memory, sensor information.. with Android application app, e.g. “CPU-Z”.
Again, I tested installing CPU-Z app for Android phone app by using this Technique.
I test with SONY BRAVIA 2015 model Android TV, “KDL-43W800C“.
CPU information
Kernel Version, Memory (RAM), Internal Storage (HDD info) are available.
* This post is workaround for testing Android TV. It does not mean that this post guarantee the behavior of introduced apps on Android TV. The tips written here is not officially supported, and I don’t take any responsibility caused by using this Technique. Try it with your own responsibility.
Create GuidedStepActivity & Implement onItemClicked in MainFragment
At first, create GuidedStepActivity by right click your package name New → Java Class → type “GuidedStepActivity” in class name. Note that this GuidedStepActivity doesn’t require res/layout xml file, so it is ok to only create Java class. Make this class subclass of Activity (write ” extends Activity” in class declaration).
Then, let’s make a button to launch this GuidedStepActivity.
private final class ItemViewClickedListener implements OnItemViewClickedListener {
@Override
public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
RowPresenter.ViewHolder rowViewHolder, Row row) {
// each time the item is clicked, code inside here will be executed.
if (item instanceof Movie) {
Movie movie = (Movie) item;
Log.d(TAG, "Item: " + item.toString());
Intent intent = new Intent(getActivity(), DetailsActivity.class);
intent.putExtra(DetailsActivity.MOVIE, movie);
getActivity().startActivity(intent);
} else if (item instanceof String){
if (item == "ErrorFragment") {
Intent intent = new Intent(getActivity(), ErrorActivity.class);
startActivity(intent);
} else if (item == "GuidedStepFragment") {
Intent intent = new Intent(getActivity(), GuidedStepActivity.class);
startActivity(intent);
}
}
}
private void loadRows() {
mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
/* GridItemPresenter */
HeaderItem gridItemPresenterHeader = new HeaderItem(0, "GridItemPresenter");
GridItemPresenter mGridPresenter = new GridItemPresenter();
ArrayObjectAdapter gridRowAdapter = new ArrayObjectAdapter(mGridPresenter);
gridRowAdapter.add("ErrorFragment");
gridRowAdapter.add("GuidedStepFragment");
gridRowAdapter.add("ITEM 3");
mRowsAdapter.add(new ListRow(gridItemPresenterHeader, gridRowAdapter));
...
}
Add declaration of this Activity to AndroidManifest.
So we can start implementing GuidedStepFragment from here. Before real implementation, I will explain a basic structure of this GuidedStepFragment.
GuidedStepFragment – Structure
As mentioned in official doc, It is composed of a guidance view on the left and a view on the right containing a list of possible actions.
Overriding Method
To use GuidedStepFragment, we need to override at least these 3 methods.
onCreateGuidance(Bundle) – To create guidance view (left side). – Attributes of guidance (title, description etc) are specified here.
onCreateActions(List, Bundle) – To define list of possible actions (right side). – Attributes of action are specified here.
onGuidedActionClicked(GuidedAction) – This is onClick listener. – Behaviors after clicking action buttons can be specified here.
At least if you know this, you can use GuidedStepFragment. But you may want to modify the design layout of this Guidance. You can use “Theme” & “Stylist” to customize visual styling
To attach instance of GuidedStepFragment, we can use GuidedStepFragment.add function. Here instance of FirstStepFragment class, which is a subclass of GuidedStepFragment, is added at onCreate of GuidedStepActivity.
Please also check the sample implementation of 3 overriding methods such as, onCreateGuidance, onCreateActions and onGuidedActionClicked.
package com.corochann.androidtvapptutorial;
import android.app.Activity;
import android.app.FragmentManager;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v17.leanback.app.GuidedStepFragment;
import android.support.v17.leanback.widget.GuidanceStylist;
import android.support.v17.leanback.widget.GuidedAction;
import android.util.Log;
import java.util.List;
/**
* Created by corochann on 24/7/2015.
*/
public class GuidedStepActivity extends Activity {
private static final String TAG = GuidedStepActivity.class.getSimpleName();
/* Action ID definition */
private static final int ACTION_CONTINUE = 0;
private static final int ACTION_BACK = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d(TAG, "onCreate");
super.onCreate(savedInstanceState);
if (null == savedInstanceState) {
GuidedStepFragment.add(getFragmentManager(), new FirstStepFragment());
}
}
private static void addAction(List actions, long id, String title, String desc) {
actions.add(new GuidedAction.Builder()
.id(id)
.title(title)
.description(desc)
.build());
}
public static class FirstStepFragment extends GuidedStepFragment {
@NonNull
@Override
public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) {
String title = "Title";
String breadcrumb = "Breadcrumb";
String description = "Description";
Drawable icon = getActivity().getDrawable(R.drawable.ic_main_icon);
return new GuidanceStylist.Guidance(title, description, breadcrumb, icon);
}
@Override
public void onCreateActions(@NonNull List actions, Bundle savedInstanceState) {
addAction(actions, ACTION_CONTINUE, "Continue", "Go to SecondStepFragment");
addAction(actions, ACTION_BACK, "Cancel", "Go back");
}
@Override
public void onGuidedActionClicked(GuidedAction action) {
switch ((int) action.getId()){
case ACTION_CONTINUE:
// FragmentManager fm = getFragmentManager();
// GuidedStepFragment.add(fm, new SecondStepFragment());
break;
case ACTION_BACK:
getActivity().finish();
break;
default:
Log.w(TAG, "Action is not defined");
break;
}
}
}
}
GuidedStepFragments must have access to an appropriate theme in order for the stylists to function properly. Specifically, the fragment must receive Theme_Leanback_GuidedStep, or a theme whose parent is set to that theme.
Another way to specify theme is to override GuidedStepFragment.onProvideTheme method.
public static class FirstStepFragment extends GuidedStepFragment {
@Override
public int onProvideTheme() {
return R.style.Theme_Example_Leanback_GuidedStep_First;
}
That’s all for implementation of FirstStepFragment, let’s proceed to implment SecondStepFragment.
SecondStepFragment
At first let’s implement 3 override methods, onCreateGuidance, onCreateActions and onGuidedActionClicked.
/* Action set ID */
private static final int OPTION_CHECK_SET_ID = 10;
/* Options of SecondStepFragment */
private static final String[] OPTION_NAMES = {"Option A", "Option B", "Option C"};
private static final String[] OPTION_DESCRIPTIONS = {"Here's one thing you can do",
"Here's another thing you can do", "Here's one more thing you can do"};
private static final int[] OPTION_DRAWABLES = {R.drawable.ic_guidedstep_option_a,
R.drawable.ic_guidedstep_option_b, R.drawable.ic_guidedstep_option_c};
private static final boolean[] OPTION_CHECKED = {true, false, false};
...
public static class SecondStepFragment extends GuidedStepFragment {
@NonNull
@Override
public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) {
String title = "SecondStepFragment";
String breadcrumb = "Guided Steps: 2";
String description ="Showcasing different action configurations";
Drawable icon = getActivity().getDrawable(R.drawable.ic_main_icon);
return new GuidanceStylist.Guidance(title, description, breadcrumb, icon);
}
@Override
public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
String title = "infoOnly action";
String desc = "infoOnly indicates whether this action is for information purposes only and cannot be clicked.\n" +
"The description can be long, by set multilineDescription to true";
actions.add(new GuidedAction.Builder()
.title(title)
.description(desc)
.multilineDescription(true)
.infoOnly(true)
.enabled(false)
.build());
for (int i = 0; i < OPTION_NAMES.length; i++) {
addCheckedAction(actions,
OPTION_DRAWABLES[i],
getActivity(),
OPTION_NAMES[i],
OPTION_DESCRIPTIONS[i],
OPTION_CHECKED[i]);
}
}
@Override
public void onGuidedActionClicked(GuidedAction action) {
String text = OPTION_NAMES[getSelectedActionPosition() - 1] + " clicked";
Toast.makeText(getActivity(), text, Toast.LENGTH_SHORT).show();
}
}
Radio-button type Action
addCheckedAction method in onCreateActions will make radio button type action list.
checkSetId method is the key to achieve this. When you specify actions with same setId, you can select only one of this action.
GuidedStepFragment – Stylist
Finally, customize stylist. To customize left side guidance view, call onCreateGuidanceStylist.
public static class SecondStepFragment extends GuidedStepFragment {
...
@Override
public GuidanceStylist onCreateGuidanceStylist() {
return new GuidanceStylist() {
@Override
public int onProvideLayoutId() {
return R.layout.guidedstep_second_guidance;
}
};
}
}
Sample layout file looks like this, so you can specify layout of the guidance view inside this layout file.
At the time of writing, official spotify app for Android TV is not released yet.
But I tested installing Spotify app for Android phone app by using this Technique.
As a result, I could stream music without any big problem. At the first time of launching app, you need to log in. You may need to use USB mouse & USB keyboard because remote controller is not enough to press some button sometimes.
Also, Now playing card will appear when you go back to Leanback Launcher display. Further more, you can use skipToNext & skipToPrevious Media button at Leanback Launcher display!!
What is nice if you stream music from TV rather than smartphones?
Of course, sound is much bigger & you can enjoy music with family or friends! If your TV is connected with Home theater system, 5.1 ch sound system… you can also enjoy music in Spotify with DYNAMIC sound, which is awesome 🙂
* This post is workaround for testing Android TV. It does not mean that this post guarantee the behavior of introduced apps on Android TV. The tips written here is not officially supported, and I don’t take any responsibility caused by this Technique. Try it with your own responsibility.
Unofficial apps for Spotify are already available
Actually, you can already find Unofficial spotify apps for Android TV in Google play store. For example, I found “TV Player for Spotify”.
It is quite easy to use, and design is optimized for Android TV usage.