Skip to content

Instantly share code, notes, and snippets.

@kingargyle
Created August 5, 2014 13:19
Show Gist options
  • Save kingargyle/a0d341f532b37a73543d to your computer and use it in GitHub Desktop.
Save kingargyle/a0d341f532b37a73543d to your computer and use it in GitHub Desktop.
Android TV Recommendation Handlers.
<!-- Required for on Android TV to receive Boot Completed events -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!-- Need to Register the Recommendation Intent Service -->
<service android:name="us.nineworlds.serenity.core.services.OnDeckRecommendationIntentService"
android:enabled="true" android:exported="true"/>
<!-- The Receiver that actually responds to the Boot Completed event, needed
to start the recommendation service automatically when bootup has
been completed, and schedules future events for every half hour. -->
<receiver android:name="us.nineworlds.serenity.StartupBroadcastReceiver" >
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
/**
* The MIT License (MIT)
* Copyright (c) 2012 David Carver
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
* OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
* OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package us.nineworlds.serenity.core.services;
import us.nineworlds.serenity.core.OnDeckRecommendations;
import android.app.IntentService;
import android.content.Intent;
/**
* @author dcarver
*
*/
public class OnDeckRecommendationIntentService extends IntentService {
/**
* An intent service that can be used in a Scheduler to fetch new
* recommendations. It's sole job is to call the actual
* OnDeckRecommendation class and the recommend method.
*
* Activities can call this service instead of using an AsyncTask
* this way requests are serialized.
*
*/
public OnDeckRecommendationIntentService() {
super("OnDeckRecommendationIntentService");
}
@Override
protected void onHandleIntent(Intent intent) {
OnDeckRecommendations onDeckRecommendations = new OnDeckRecommendations(
this);
onDeckRecommendations.recommend();
}
}
/**
* The MIT License (MIT)
* Copyright (c) 2012 David Carver
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
* OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
* OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package us.nineworlds.serenity.core;
import java.io.IOException;
import java.util.List;
import us.nineworlds.plex.rest.PlexappFactory;
import us.nineworlds.plex.rest.model.impl.MediaContainer;
import us.nineworlds.serenity.R;
import us.nineworlds.serenity.SerenityApplication;
import us.nineworlds.serenity.core.menus.MenuItem;
import us.nineworlds.serenity.core.model.VideoContentInfo;
import us.nineworlds.serenity.core.model.impl.EpisodeMediaContainer;
import us.nineworlds.serenity.core.model.impl.EpisodePosterInfo;
import us.nineworlds.serenity.core.model.impl.MenuMediaContainer;
import us.nineworlds.serenity.core.model.impl.MovieMediaContainer;
import us.nineworlds.serenity.core.model.impl.MoviePosterInfo;
import us.nineworlds.serenity.ui.video.player.RecommendationPlayerActivity;
import us.nineworlds.serenity.volley.DefaultLoggingVolleyErrorListener;
import us.nineworlds.serenity.volley.VolleyUtils;
import android.annotation.TargetApi;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.TaskStackBuilder;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Build;
import android.util.Log;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
/**
* Due to the use of PendingIntent this requires Jelly Bean or higher to run
*/
public class OnDeckRecommendations {
Context context;
RequestQueue queue;
final PlexappFactory factory = SerenityApplication.getPlexFactory();
NotificationManager notificationManager;
protected List<MenuItem> menuItems;
public OnDeckRecommendations(Context context) {
this.context = context;
queue = VolleyUtils.getRequestQueueInstance(context);
}
public void recommend() {
String sectionsURL = factory.getSectionsURL();
VolleyUtils.volleyXmlGetRequest(sectionsURL,
new LibraryResponseListener(),
new DefaultLoggingVolleyErrorListener());
}
protected class LibraryResponseListener implements
Response.Listener<MediaContainer> {
@Override
public void onResponse(MediaContainer mc) {
menuItems = new MenuMediaContainer(mc, context).createMenuItems();
if (menuItems.isEmpty()) {
return;
}
for (MenuItem library : menuItems) {
if ("movie".equals(library.getType())) {
String section = library.getSection();
String onDeckURL = factory
.getSectionsURL(section, "onDeck");
VolleyUtils.volleyXmlGetRequest(onDeckURL,
new MovieOnDeckResponseListener(),
new DefaultLoggingVolleyErrorListener());
}
if ("show".equals(library.getType())) {
String section = library.getSection();
String onDeckUrl = factory
.getSectionsURL(section, "onDeck");
VolleyUtils.volleyXmlGetRequest(onDeckUrl,
new TVOnDeckResponseListener(),
new DefaultLoggingVolleyErrorListener());
}
}
}
}
protected class MovieOnDeckResponseListener implements
Response.Listener<MediaContainer> {
@Override
public void onResponse(MediaContainer mc) {
MovieMediaContainer movieContainer = new MovieMediaContainer(mc);
List<VideoContentInfo> movies = movieContainer.createVideos();
for (VideoContentInfo movie : movies) {
new RecommendAsyncTask(movie, context).execute();
}
}
}
protected class TVOnDeckResponseListener implements
Response.Listener<MediaContainer> {
@Override
public void onResponse(MediaContainer mc) {
EpisodeMediaContainer episodeContainer = new EpisodeMediaContainer(
mc, context);
List<VideoContentInfo> episodes = episodeContainer.createVideos();
for (VideoContentInfo episode : episodes) {
new RecommendAsyncTask(episode, context).execute();
}
}
}
protected class RecommendAsyncTask extends AsyncTask {
private final VideoContentInfo video;
private final Context context;
private final PlexappFactory factory = SerenityApplication
.getPlexFactory();
public RecommendAsyncTask(VideoContentInfo video, Context context) {
this.video = video;
this.context = context;
}
@Override
protected Object doInBackground(Object... params) {
RecommendationBuilder builder = new RecommendationBuilder();
try {
PendingIntent intent = buildPendingIntent(video);
String backgroundURL = factory.getImageURL(
video.getBackgroundURL(), 1920, 1080);
int priority = 0;
if (video.viewedPercentage() > 0.0f) {
priority = Math.round(100 * video.viewedPercentage());
}
builder.setContext(context).setBackground(backgroundURL)
.setTitle(video.getTitle())
.setImage(video.getImageURL())
.setId(Integer.parseInt(video.id()))
.setPriority(priority)
.setDescription(video.getSummary())
.setSmallIcon(R.drawable.androidtv_icon_mono)
.setIntent(intent).build();
} catch (IOException ex) {
Log.e("OnDeckRecommendation", "Error building recommendation: "
+ builder.toString(), ex);
}
return null;
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private PendingIntent buildPendingIntent(VideoContentInfo video) {
Intent intent = new Intent(context,
RecommendationPlayerActivity.class);
// Need to pass in the concerte class instead of the Interface for the Serializable implementation.
if (video instanceof MoviePosterInfo) {
intent.putExtra("serenity_video", (MoviePosterInfo) video);
}
if (video instanceof EpisodePosterInfo) {
intent.putExtra("serenity_video", (EpisodePosterInfo) video);
}
TaskStackBuilder stackBuilder = TaskStackBuilder.create(context);
stackBuilder.addNextIntent(intent);
// Ensure a unique PendingIntents, otherwise all recommendations end
// up with the same
// PendingIntent
intent.setAction(video.id());
PendingIntent pintent = stackBuilder.getPendingIntent(0,
PendingIntent.FLAG_UPDATE_CURRENT);
return pintent;
}
}
}
/*
* 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.
*/
/**
* Based on code from the androidTV-leanback sample project.
*
*/
package us.nineworlds.serenity.core;
import java.io.IOException;
import us.nineworlds.serenity.SerenityApplication;
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.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.assist.ImageSize;
/*
* This class builds recommendations as notifications with videos as inputs.
*/
public class RecommendationBuilder {
private static final String TAG = "RecommendationBuilder";
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);
}
ImageLoader imageLoader = SerenityApplication.getImageLoader();
Bitmap image = imageLoader.loadImageSync(mImageUri, new ImageSize(300,
400), SerenityApplication.getSycnOptions());
Notification notification = new NotificationCompat.BigPictureStyle(
new NotificationCompat.Builder(mContext)
.setContentTitle(mTitle)
.setContentText(mDescription)
.setPriority(mPriority)
.setOngoing(true)
.setLocalOnly(true)
.setColor(
mContext.getResources()
.getColor(
us.nineworlds.serenity.R.color.holo_color))
.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 + '}';
}
}
/**
* The MIT License (MIT)
* Copyright (c) 2012 David Carver
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
* OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
* OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package us.nineworlds.serenity;
import us.nineworlds.serenity.core.services.OnDeckRecommendationIntentService;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
import android.preference.PreferenceManager;
/**
* Used to automatically launch Serenity for Android after boot is completed on
* a device. This is only enabled if the startup preference option has been set
* to true.
*
* Starts the OnDeckRecommendationService if running on Jelly Bean or higher devices.
*
* @author dcarver
*
*/
public class StartupBroadcastReceiver extends BroadcastReceiver {
private static final int INITIAL_DELAY = 5000;
Context context;
@Override
public void onReceive(Context context, Intent intent) {
this.context = context;
if (intent.getAction() == null) {
return;
}
if (intent.getAction().equals("android.intent.action.BOOT_COMPLETED")) {
createRecomendations();
launchSerenityOnStartup();
}
}
/**
* @param context
*/
protected void launchSerenityOnStartup() {
SharedPreferences prefs = PreferenceManager
.getDefaultSharedPreferences(context);
boolean startupAfterBoot = prefs.getBoolean("serenity_boot_startup",
false);
if (startupAfterBoot) {
Intent i = new Intent(context, MainActivity.class);
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(i);
}
}
protected void createRecomendations() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
return;
}
AlarmManager alarmManager = (AlarmManager) context
.getSystemService(Context.ALARM_SERVICE);
Intent recommendationIntent = new Intent(context,
OnDeckRecommendationIntentService.class);
PendingIntent alarmIntent = PendingIntent.getService(context, 0,
recommendationIntent, 0);
alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
INITIAL_DELAY, AlarmManager.INTERVAL_HALF_HOUR, alarmIntent);
}
}
/**
* The MIT License (MIT)
* Copyright (c) 2012-2014 David Carver
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
* OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
* OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package us.nineworlds.serenity.volley;
import us.nineworlds.plex.rest.model.impl.MediaContainer;
import us.nineworlds.serenity.core.OkHttpStack;
import android.content.Context;
import android.util.Log;
import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.toolbox.Volley;
/**
* @author dcarver
*
*/
public class VolleyUtils {
private static RequestQueue queue;
public static RequestQueue getRequestQueueInstance(Context context) {
if (queue == null) {
queue = Volley.newRequestQueue(context, new OkHttpStack());
}
return queue;
}
public static Request volleyXmlGetRequest(String url,
Response.Listener response, Response.ErrorListener error) {
SimpleXmlRequest<MediaContainer> request = new SimpleXmlRequest<MediaContainer>(
Request.Method.GET, url, MediaContainer.class, response, error);
if (queue == null) {
Log.e("VolleyUtils", "Initialize Request Queue!");
return null;
}
return queue.add(request);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment