aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--tv/ChannelsPrograms/Application/.gitignore16
-rw-r--r--tv/ChannelsPrograms/Application/proguard-project.txt20
-rw-r--r--tv/ChannelsPrograms/Application/src/main/AndroidManifest.xml89
-rw-r--r--tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/AppLinkActivity.java117
-rw-r--r--tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/InitializeChannelsReceiver.java36
-rw-r--r--tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/MainActivity.java132
-rw-r--r--tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/SyncChannelJobService.java100
-rw-r--r--tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/SyncProgramsJobService.java241
-rw-r--r--tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/model/MockDatabase.java273
-rw-r--r--tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/model/MockMovieService.java173
-rw-r--r--tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/model/Movie.java143
-rw-r--r--tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/model/Subscription.java80
-rw-r--r--tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/playback/PlaybackActivity.java36
-rw-r--r--tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/playback/PlaybackVideoFragment.java135
-rw-r--r--tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/playback/SimplePlaybackTransportControlGlue.java160
-rw-r--r--tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/playback/WatchNextAdapter.java116
-rw-r--r--tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/util/AppLinkHelper.java215
-rw-r--r--tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/util/SharedPreferencesHelper.java147
-rw-r--r--tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/util/TvUtil.java203
-rw-r--r--tv/ChannelsPrograms/Application/src/main/res/drawable/app_icon_your_company.pngbin0 -> 12416 bytes
-rw-r--r--tv/ChannelsPrograms/Application/src/main/res/drawable/ic_movie_blue_80dp.xml24
-rw-r--r--tv/ChannelsPrograms/Application/src/main/res/drawable/ic_video_library_blue_80dp.xml24
-rw-r--r--tv/ChannelsPrograms/Application/src/main/res/layout/activity_main.xml139
-rw-r--r--tv/ChannelsPrograms/Application/src/main/res/layout/playback_controls.xml26
-rw-r--r--tv/ChannelsPrograms/Application/src/main/res/mipmap-hdpi/ic_launcher.pngbin0 -> 3418 bytes
-rw-r--r--tv/ChannelsPrograms/Application/src/main/res/mipmap-mdpi/ic_launcher.pngbin0 -> 2206 bytes
-rw-r--r--tv/ChannelsPrograms/Application/src/main/res/mipmap-xhdpi/ic_launcher.pngbin0 -> 4842 bytes
-rw-r--r--tv/ChannelsPrograms/Application/src/main/res/mipmap-xxhdpi/ic_launcher.pngbin0 -> 7718 bytes
-rw-r--r--tv/ChannelsPrograms/Application/src/main/res/values/strings.xml39
-rw-r--r--tv/ChannelsPrograms/build.gradle12
-rw-r--r--tv/ChannelsPrograms/buildSrc/build.gradle16
-rw-r--r--tv/ChannelsPrograms/gradle/wrapper/gradle-wrapper.jarbin0 -> 49896 bytes
-rw-r--r--tv/ChannelsPrograms/gradle/wrapper/gradle-wrapper.properties6
-rwxr-xr-xtv/ChannelsPrograms/gradlew164
-rw-r--r--tv/ChannelsPrograms/gradlew.bat90
-rw-r--r--tv/ChannelsPrograms/settings.gradle2
-rw-r--r--tv/ChannelsPrograms/template-params.xml116
37 files changed, 3090 insertions, 0 deletions
diff --git a/tv/ChannelsPrograms/Application/.gitignore b/tv/ChannelsPrograms/Application/.gitignore
new file mode 100644
index 00000000..8232fc36
--- /dev/null
+++ b/tv/ChannelsPrograms/Application/.gitignore
@@ -0,0 +1,16 @@
+# Copyright 2017 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.
+src/template/
+src/common/
+build.gradle
diff --git a/tv/ChannelsPrograms/Application/proguard-project.txt b/tv/ChannelsPrograms/Application/proguard-project.txt
new file mode 100644
index 00000000..f2fe1559
--- /dev/null
+++ b/tv/ChannelsPrograms/Application/proguard-project.txt
@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# 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 *;
+#}
diff --git a/tv/ChannelsPrograms/Application/src/main/AndroidManifest.xml b/tv/ChannelsPrograms/Application/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..7f941140
--- /dev/null
+++ b/tv/ChannelsPrograms/Application/src/main/AndroidManifest.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2017 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.tv.channelsprograms"
+ android:versionCode="1"
+ android:versionName="1.0">
+
+
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
+ <uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA" />
+ <uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" />
+
+ <uses-feature
+ android:name="android.hardware.touchscreen"
+ android:required="false" />
+ <uses-feature
+ android:name="android.software.leanback"
+ android:required="true" />
+ <uses-feature
+ android:name="android.hardware.microphone"
+ android:required="false" />
+
+ <application android:allowBackup="true"
+ android:label="@string/app_name"
+ android:icon="@mipmap/ic_launcher"
+ android:supportsRtl="true"
+ android:theme="@style/Theme.Leanback">
+
+ <activity
+ android:name=".MainActivity"
+ android:banner="@drawable/app_icon_your_company"
+ android:icon="@drawable/app_icon_your_company"
+ android:label="@string/app_name"
+ android:logo="@drawable/app_icon_your_company"
+ 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=".AppLinkActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data
+ android:host="app"
+ android:scheme="tvrecommendation" />
+ </intent-filter>
+ </activity>
+ <activity android:name=".playback.PlaybackActivity" />
+
+ <receiver android:name=".InitializeChannelsReceiver">
+ <intent-filter>
+ <action android:name="android.media.tv.action.INITIALIZE_PROGRAMS" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </receiver>
+
+ <service
+ android:name=".SyncChannelJobService"
+ android:exported="false"
+ android:permission="android.permission.BIND_JOB_SERVICE" />
+
+ <service
+ android:name=".SyncProgramsJobService"
+ android:exported="false"
+ android:permission="android.permission.BIND_JOB_SERVICE" />
+
+ </application>
+
+</manifest>
diff --git a/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/AppLinkActivity.java b/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/AppLinkActivity.java
new file mode 100644
index 00000000..7b1d1393
--- /dev/null
+++ b/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/AppLinkActivity.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2017 Google Inc.
+ *
+ * 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.tv.channelsprograms;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.example.android.tv.channelsprograms.model.MockDatabase;
+import com.example.android.tv.channelsprograms.model.Movie;
+import com.example.android.tv.channelsprograms.model.Subscription;
+import com.example.android.tv.channelsprograms.playback.PlaybackActivity;
+import com.example.android.tv.channelsprograms.util.AppLinkHelper;
+
+/**
+ * Delegates to the correct activity based on how the user entered the app.
+ *
+ * <p>Supports two options: view and play. The view option will open the channel for the user to be
+ * able to view more programs. The play option will load the channel/program,
+ * subscriptions/mediaContent start playing the movie.
+ */
+public class AppLinkActivity extends Activity {
+
+ private static final String TAG = "AppLinkActivity";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Intent intent = getIntent();
+ Uri uri = intent.getData();
+
+ Log.v(TAG, uri.toString());
+
+ if (uri.getPathSegments().isEmpty()) {
+ Log.e(TAG, "Invalid uri " + uri);
+ finish();
+ return;
+ }
+
+ AppLinkHelper.AppLinkAction action = AppLinkHelper.extractAction(uri);
+ switch (action.getAction()) {
+ case AppLinkHelper.PLAYBACK:
+ play((AppLinkHelper.PlaybackAction) action);
+ break;
+ case AppLinkHelper.BROWSE:
+ browse((AppLinkHelper.BrowseAction) action);
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid Action " + action);
+ }
+ }
+
+ private void browse(AppLinkHelper.BrowseAction action) {
+ Subscription subscription =
+ MockDatabase.findSubscriptionByName(this, action.getSubscriptionName());
+ if (subscription == null) {
+ Log.e(TAG, "Invalid subscription " + action.getSubscriptionName());
+ } else {
+ // TODO: Open an activity that has the movies for the subscription.
+ Toast.makeText(this, action.getSubscriptionName(), Toast.LENGTH_LONG).show();
+ }
+ finish();
+ }
+
+ private void play(AppLinkHelper.PlaybackAction action) {
+ if (action.getPosition() == AppLinkHelper.DEFAULT_POSITION) {
+ Log.d(
+ TAG,
+ "Playing program "
+ + action.getMovieId()
+ + " from channel "
+ + action.getChannelId());
+ } else {
+ Log.d(
+ TAG,
+ "Continuing program "
+ + action.getMovieId()
+ + " from channel "
+ + action.getChannelId()
+ + " at time "
+ + action.getPosition());
+ }
+
+ Movie movie = MockDatabase.findMovieById(this, action.getChannelId(), action.getMovieId());
+ if (movie == null) {
+ Log.e(TAG, "Invalid program " + action.getMovieId());
+ } else {
+ startPlaying(action.getChannelId(), movie, action.getPosition());
+ }
+ finish();
+ }
+
+ private void startPlaying(long channelId, Movie movie, long position) {
+ Intent playMovieIntent = new Intent(this, PlaybackActivity.class);
+ playMovieIntent.putExtra(PlaybackActivity.EXTRA_MOVIE, movie);
+ playMovieIntent.putExtra(PlaybackActivity.EXTRA_CHANNEL_ID, channelId);
+ playMovieIntent.putExtra(PlaybackActivity.EXTRA_POSITION, position);
+ startActivity(playMovieIntent);
+ }
+}
diff --git a/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/InitializeChannelsReceiver.java b/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/InitializeChannelsReceiver.java
new file mode 100644
index 00000000..7ddb2f9c
--- /dev/null
+++ b/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/InitializeChannelsReceiver.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2017 Google Inc.
+ *
+ * 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.tv.channelsprograms;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import com.example.android.tv.channelsprograms.util.TvUtil;
+
+/** Initializes channels and programs at installation time. */
+public class InitializeChannelsReceiver extends BroadcastReceiver {
+
+ private static final String TAG = "InitializeChannelsRcvr";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.d(TAG, "onReceive(): " + intent);
+
+ TvUtil.scheduleSyncingChannel(context);
+ }
+}
diff --git a/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/MainActivity.java b/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/MainActivity.java
new file mode 100644
index 00000000..04f09909
--- /dev/null
+++ b/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/MainActivity.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2017 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.tv.channelsprograms;
+
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.media.tv.TvContractCompat;
+import android.util.Log;
+import android.widget.Button;
+import android.widget.Toast;
+
+import com.example.android.tv.channelsprograms.model.MockDatabase;
+import com.example.android.tv.channelsprograms.model.Subscription;
+import com.example.android.tv.channelsprograms.util.TvUtil;
+
+import java.util.Arrays;
+import java.util.List;
+
+/*
+ * Displays subscriptions that can be added to the main launcher's channels.
+ */
+public class MainActivity extends Activity {
+
+ private static final String TAG = "MainActivity";
+
+ private static final int MAKE_BROWSABLE_REQUEST_CODE = 9001;
+
+ private Button mTvSubscribeButton;
+ private Button mVideoClipSubscribeButton;
+ private Button mCatVideosSubscribeButton;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ mTvSubscribeButton = findViewById(R.id.subscribe_tv_button);
+ mVideoClipSubscribeButton = findViewById(R.id.subscribe_video_button);
+ mCatVideosSubscribeButton = findViewById(R.id.subscribe_cat_videos_button);
+
+ final Subscription tvShowSubscription =
+ MockDatabase.getTvShowSubscription(getApplicationContext());
+ setupButtonState(mTvSubscribeButton, tvShowSubscription);
+
+ final Subscription videoSubscription =
+ MockDatabase.getVideoSubscription(getApplicationContext());
+ setupButtonState(mVideoClipSubscribeButton, videoSubscription);
+
+ final Subscription catVideosSubscription =
+ MockDatabase.getCatVideosSubscription(getApplicationContext());
+ setupButtonState(mCatVideosSubscribeButton, catVideosSubscription);
+
+ TvUtil.scheduleSyncingChannel(this);
+ }
+
+ private void setupButtonState(Button button, final Subscription subscription) {
+ boolean channelExists = subscription.getChannelId() > 0L;
+ button.setEnabled(!channelExists);
+ button.setOnClickListener(
+ view -> new AddChannelTask(getApplicationContext()).execute(subscription));
+ }
+
+ private class AddChannelTask extends AsyncTask<Subscription, Void, Long> {
+
+ private final Context mContext;
+
+ AddChannelTask(Context context) {
+ this.mContext = context;
+ }
+
+ @Override
+ protected Long doInBackground(Subscription... varArgs) {
+ List<Subscription> subscriptions = Arrays.asList(varArgs);
+ if (subscriptions.size() != 1) {
+ return -1L;
+ }
+ Subscription subscription = subscriptions.get(0);
+ // TODO: step 16 create channel. Replace declaration with code from code lab.
+ long channelId = TvUtil.createChannel(mContext, subscription);
+
+ subscription.setChannelId(channelId);
+ MockDatabase.saveSubscription(mContext, subscription);
+ // Scheduler listen on channel's uri. Updates after the user interacts with the system
+ // dialog.
+ TvUtil.scheduleSyncingProgramsForChannel(getApplicationContext(), channelId);
+ return channelId;
+ }
+
+ @Override
+ protected void onPostExecute(Long channelId) {
+ super.onPostExecute(channelId);
+ promptUserToDisplayChannel(channelId);
+ }
+ }
+
+ private void promptUserToDisplayChannel(long channelId) {
+ // TODO: step 17 prompt user.
+ Intent intent = new Intent(TvContractCompat.ACTION_REQUEST_CHANNEL_BROWSABLE);
+ intent.putExtra(TvContractCompat.EXTRA_CHANNEL_ID, channelId);
+ try {
+ this.startActivityForResult(intent, MAKE_BROWSABLE_REQUEST_CODE);
+ } catch (ActivityNotFoundException e) {
+ Log.e(TAG, "Could not start activity: " + intent.getAction(), e);
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ // TODO step 18 handle response
+ if (resultCode == RESULT_OK) {
+ Toast.makeText(this, R.string.channel_added, Toast.LENGTH_LONG).show();
+ } else {
+ Toast.makeText(this, R.string.channel_not_added, Toast.LENGTH_LONG).show();
+ }
+ }
+}
diff --git a/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/SyncChannelJobService.java b/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/SyncChannelJobService.java
new file mode 100644
index 00000000..45d4a3e4
--- /dev/null
+++ b/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/SyncChannelJobService.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2017 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.tv.channelsprograms;
+
+import android.app.job.JobParameters;
+import android.app.job.JobService;
+import android.content.Context;
+import android.os.AsyncTask;
+import android.support.media.tv.TvContractCompat;
+import android.util.Log;
+
+import com.example.android.tv.channelsprograms.model.MockDatabase;
+import com.example.android.tv.channelsprograms.model.MockMovieService;
+import com.example.android.tv.channelsprograms.model.Subscription;
+import com.example.android.tv.channelsprograms.util.TvUtil;
+
+import java.util.List;
+
+/**
+ * Populates the TV provider with channels that every user should have. Once a channel is created,
+ * it triggers another service to add programs.
+ */
+public class SyncChannelJobService extends JobService {
+
+ private static final String TAG = "RecommendChannelJobSvc";
+
+ private SyncChannelTask mSyncChannelTask;
+
+ @Override
+ public boolean onStartJob(final JobParameters jobParameters) {
+ Log.d(TAG, "Starting channel creation job");
+ mSyncChannelTask =
+ new SyncChannelTask(getApplicationContext()) {
+ @Override
+ protected void onPostExecute(Boolean success) {
+ super.onPostExecute(success);
+ jobFinished(jobParameters, !success);
+ }
+ };
+ mSyncChannelTask.execute();
+ return true;
+ }
+
+ @Override
+ public boolean onStopJob(JobParameters jobParameters) {
+ if (mSyncChannelTask != null) {
+ mSyncChannelTask.cancel(true);
+ }
+ return true;
+ }
+
+ private static class SyncChannelTask extends AsyncTask<Void, Void, Boolean> {
+
+ private final Context mContext;
+
+ SyncChannelTask(Context context) {
+ this.mContext = context;
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... voids) {
+ List<Subscription> subscriptions = MockDatabase.getSubscriptions(mContext);
+ int numOfChannelsInTVProvider = TvUtil.getNumberOfChannels(mContext);
+ // Checks if the default channels are added. Since a user can add more channels from
+ // your app later, the number of channels in the provider can be greater than the number
+ // of default channels.
+ if (numOfChannelsInTVProvider >= subscriptions.size() && !subscriptions.isEmpty()) {
+ Log.d(TAG, "Already loaded default channels into the provider");
+ } else {
+ // Create subscriptions from mocked source.
+ subscriptions = MockMovieService.createUniversalSubscriptions(mContext);
+ for (Subscription subscription : subscriptions) {
+ long channelId = TvUtil.createChannel(mContext, subscription);
+ subscription.setChannelId(channelId);
+ TvContractCompat.requestChannelBrowsable(mContext, channelId);
+ }
+
+ MockDatabase.saveSubscriptions(mContext, subscriptions);
+ }
+
+ // Kick off a job to update default programs.
+ // The program job should verify if the channel is visible before updating programs.
+ for (Subscription channel : subscriptions) {
+ TvUtil.scheduleSyncingProgramsForChannel(mContext, channel.getChannelId());
+ }
+ return true;
+ }
+ }
+}
diff --git a/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/SyncProgramsJobService.java b/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/SyncProgramsJobService.java
new file mode 100644
index 00000000..47fde6a6
--- /dev/null
+++ b/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/SyncProgramsJobService.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2017 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.tv.channelsprograms;
+
+import android.app.job.JobParameters;
+import android.app.job.JobService;
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.PersistableBundle;
+import android.support.annotation.NonNull;
+import android.support.media.tv.Channel;
+import android.support.media.tv.PreviewProgram;
+import android.support.media.tv.TvContractCompat;
+import android.util.Log;
+
+import com.example.android.tv.channelsprograms.model.MockDatabase;
+import com.example.android.tv.channelsprograms.model.MockMovieService;
+import com.example.android.tv.channelsprograms.model.Movie;
+import com.example.android.tv.channelsprograms.model.Subscription;
+import com.example.android.tv.channelsprograms.util.AppLinkHelper;
+import com.example.android.tv.channelsprograms.util.TvUtil;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Syncs programs for a channel. A channel id is required to be passed via the {@link
+ * JobParameters}. This service is scheduled to listen to changes to a channel. Once the job
+ * completes, it will reschedule itself to listen for the next change to the channel. See {@link
+ * TvUtil#scheduleSyncingProgramsForChannel(Context, long)} for more details about the scheduling.
+ */
+public class SyncProgramsJobService extends JobService {
+
+ private static final String TAG = "SyncProgramsJobService";
+
+ private SyncProgramsTask mSyncProgramsTask;
+
+ @Override
+ public boolean onStartJob(final JobParameters jobParameters) {
+ Log.d(TAG, "onStartJob(): " + jobParameters);
+
+ final long channelId = getChannelId(jobParameters);
+ if (channelId == -1L) {
+ return false;
+ }
+ Log.d(TAG, "onStartJob(): Scheduling syncing for programs for channel " + channelId);
+
+ mSyncProgramsTask =
+ new SyncProgramsTask(getApplicationContext()) {
+ @Override
+ protected void onPostExecute(Boolean finished) {
+ super.onPostExecute(finished);
+ // Daisy chain listening for the next change to the channel.
+ TvUtil.scheduleSyncingProgramsForChannel(
+ SyncProgramsJobService.this, channelId);
+ mSyncProgramsTask = null;
+ jobFinished(jobParameters, !finished);
+ }
+ };
+ mSyncProgramsTask.execute(channelId);
+
+ return true;
+ }
+
+ @Override
+ public boolean onStopJob(JobParameters jobParameters) {
+ if (mSyncProgramsTask != null) {
+ mSyncProgramsTask.cancel(true);
+ }
+ return true;
+ }
+
+ private long getChannelId(JobParameters jobParameters) {
+ PersistableBundle extras = jobParameters.getExtras();
+ if (extras == null) {
+ return -1L;
+ }
+
+ return extras.getLong(TvContractCompat.EXTRA_CHANNEL_ID, -1L);
+ }
+
+ /*
+ * Syncs programs by querying the given channel id.
+ *
+ * If the channel is not browsable, the programs will be removed to avoid showing
+ * stale programs when the channel becomes browsable in the future.
+ *
+ * If the channel is browsable, then it will check if the channel has any programs.
+ * If the channel does not have any programs, new programs will be added.
+ * If the channel does have programs, then a fresh list of programs will be fetched and the
+ * channel's programs will be updated.
+ */
+ private void syncPrograms(long channelId, List<Movie> initialMovies) {
+ Log.d(TAG, "Sync programs for channel: " + channelId);
+ List<Movie> movies = new ArrayList<>(initialMovies);
+
+ try (Cursor cursor =
+ getContentResolver()
+ .query(
+ TvContractCompat.buildChannelUri(channelId),
+ null,
+ null,
+ null,
+ null)) {
+ if (cursor != null && cursor.moveToNext()) {
+ Channel channel = Channel.fromCursor(cursor);
+ if (!channel.isBrowsable()) {
+ Log.d(TAG, "Channel is not browsable: " + channelId);
+ deletePrograms(channelId, movies);
+ } else {
+ Log.d(TAG, "Channel is browsable: " + channelId);
+ if (movies.isEmpty()) {
+ movies = createPrograms(channelId, MockMovieService.getList());
+ } else {
+ movies = updatePrograms(channelId, movies);
+ }
+ MockDatabase.saveMovies(getApplicationContext(), channelId, movies);
+ }
+ }
+ }
+ }
+
+ private List<Movie> createPrograms(long channelId, List<Movie> movies) {
+
+ List<Movie> moviesAdded = new ArrayList<>(movies.size());
+ for (Movie movie : movies) {
+ PreviewProgram previewProgram = buildProgram(channelId, movie);
+
+ Uri programUri =
+ getContentResolver()
+ .insert(
+ TvContractCompat.PreviewPrograms.CONTENT_URI,
+ previewProgram.toContentValues());
+ long programId = ContentUris.parseId(programUri);
+ Log.d(TAG, "Inserted new program: " + programId);
+ movie.setProgramId(programId);
+ moviesAdded.add(movie);
+ }
+
+ return moviesAdded;
+ }
+
+ private List<Movie> updatePrograms(long channelId, List<Movie> movies) {
+
+ // By getting a fresh list, we should see a visible change in the home screen.
+ List<Movie> updateMovies = MockMovieService.getFreshList();
+ for (int i = 0; i < movies.size(); ++i) {
+ Movie old = movies.get(i);
+ Movie update = updateMovies.get(i);
+ long programId = old.getProgramId();
+
+ getContentResolver()
+ .update(
+ TvContractCompat.buildPreviewProgramUri(programId),
+ buildProgram(channelId, update).toContentValues(),
+ null,
+ null);
+ Log.d(TAG, "Updated program: " + programId);
+ update.setProgramId(programId);
+ }
+
+ return updateMovies;
+ }
+
+ private void deletePrograms(long channelId, List<Movie> movies) {
+ if (movies.isEmpty()) {
+ return;
+ }
+
+ int count = 0;
+ for (Movie movie : movies) {
+ count +=
+ getContentResolver()
+ .delete(
+ TvContractCompat.buildPreviewProgramUri(movie.getProgramId()),
+ null,
+ null);
+ }
+ Log.d(TAG, "Deleted " + count + " programs for channel " + channelId);
+
+ // Remove our local records to stay in sync with the TV Provider.
+ MockDatabase.removeMovies(getApplicationContext(), channelId);
+ }
+
+ @NonNull
+ private PreviewProgram buildProgram(long channelId, Movie movie) {
+ Uri posterArtUri = Uri.parse(movie.getCardImageUrl());
+ Uri appLinkUri = AppLinkHelper.buildPlaybackUri(channelId, movie.getId());
+ Uri previewVideoUri = Uri.parse(movie.getVideoUrl());
+
+ PreviewProgram.Builder builder = new PreviewProgram.Builder();
+ builder.setChannelId(channelId)
+ .setType(TvContractCompat.PreviewProgramColumns.TYPE_CLIP)
+ .setTitle(movie.getTitle())
+ .setDescription(movie.getDescription())
+ .setPosterArtUri(posterArtUri)
+ .setPreviewVideoUri(previewVideoUri)
+ .setIntentUri(appLinkUri);
+ return builder.build();
+ }
+
+ private class SyncProgramsTask extends AsyncTask<Long, Void, Boolean> {
+
+ private final Context mContext;
+
+ private SyncProgramsTask(Context context) {
+ this.mContext = context;
+ }
+
+ @Override
+ protected Boolean doInBackground(Long... channelIds) {
+ List<Long> params = Arrays.asList(channelIds);
+ if (!params.isEmpty()) {
+ for (Long channelId : params) {
+ Subscription subscription =
+ MockDatabase.findSubscriptionByChannelId(mContext, channelId);
+ if (subscription != null) {
+ List<Movie> cachedMovies = MockDatabase.getMovies(mContext, channelId);
+ syncPrograms(channelId, cachedMovies);
+ }
+ }
+ }
+ return true;
+ }
+ }
+}
diff --git a/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/model/MockDatabase.java b/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/model/MockDatabase.java
new file mode 100644
index 00000000..f540c395
--- /dev/null
+++ b/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/model/MockDatabase.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2017 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.tv.channelsprograms.model;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.Nullable;
+import android.support.annotation.StringRes;
+
+import com.example.android.tv.channelsprograms.R;
+import com.example.android.tv.channelsprograms.util.AppLinkHelper;
+import com.example.android.tv.channelsprograms.util.SharedPreferencesHelper;
+
+import java.util.Collections;
+import java.util.List;
+
+/** Mock database stores data in {@link SharedPreferences}. */
+public final class MockDatabase {
+
+ private MockDatabase() {
+ // Do nothing.
+ }
+
+ /**
+ * Returns a subscription to mock content representing tv shows.
+ *
+ * @param context used for accessing shared preferences.
+ * @return a subscription with tv show data.
+ */
+ public static Subscription getTvShowSubscription(Context context) {
+
+ return findOrCreateSubscription(
+ context,
+ R.string.title_tv_shows,
+ R.string.tv_shows_description,
+ R.drawable.ic_video_library_blue_80dp);
+ }
+
+ /**
+ * Returns a subscription to mock content representing your videos.
+ *
+ * @param context used for accessing shared preferences.
+ * @return a subscription with your video data.
+ */
+ public static Subscription getVideoSubscription(Context context) {
+
+ return findOrCreateSubscription(
+ context,
+ R.string.your_videos,
+ R.string.your_videos_description,
+ R.drawable.ic_video_library_blue_80dp);
+ }
+
+ /**
+ * Returns a subscription to mock content representing cat videos.
+ *
+ * @param context used for accessing shared preferences.
+ * @return a subscription with cat videos.
+ */
+ public static Subscription getCatVideosSubscription(Context context) {
+
+ return findOrCreateSubscription(
+ context,
+ R.string.cat_videos,
+ R.string.cat_videos_description,
+ R.drawable.ic_movie_blue_80dp);
+ }
+
+ private static Subscription findOrCreateSubscription(
+ Context context,
+ @StringRes int titleResource,
+ @StringRes int descriptionResource,
+ @DrawableRes int logoResource) {
+ // See if we have already created the channel in the TV Provider.
+ String title = context.getString(titleResource);
+
+ Subscription subscription = findSubscriptionByTitle(context, title);
+ if (subscription != null) {
+ return subscription;
+ }
+
+ return Subscription.createSubscription(
+ title,
+ context.getString(descriptionResource),
+ AppLinkHelper.buildBrowseUri(title).toString(),
+ logoResource);
+ }
+
+ @Nullable
+ private static Subscription findSubscriptionByTitle(Context context, String title) {
+ for (Subscription subscription : getSubscriptions(context)) {
+ if (subscription.getName().equals(title)) {
+ return subscription;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Overrides the subscriptions stored in {@link SharedPreferences}.
+ *
+ * @param context used for accessing shared preferences.
+ * @param subscriptions stored in shared preferences.
+ */
+ public static void saveSubscriptions(Context context, List<Subscription> subscriptions) {
+ SharedPreferencesHelper.storeSubscriptions(context, subscriptions);
+ }
+
+ /**
+ * Adds the subscription to the list of persisted subscriptions in {@link SharedPreferences}.
+ * Will update the persisted subscription if it already exists.
+ *
+ * @param context used for accessing shared preferences.
+ * @param subscription to be saved.
+ */
+ public static void saveSubscription(Context context, Subscription subscription) {
+ List<Subscription> subscriptions = getSubscriptions(context);
+ int index = findSubscription(subscriptions, subscription);
+ if (index == -1) {
+ subscriptions.add(subscription);
+ } else {
+ subscriptions.set(index, subscription);
+ }
+ saveSubscriptions(context, subscriptions);
+ }
+
+ private static int findSubscription(
+ List<Subscription> subscriptions, Subscription subscription) {
+ for (int index = 0; index < subscriptions.size(); ++index) {
+ Subscription current = subscriptions.get(index);
+ if (current.getName().equals(subscription.getName())) {
+ return index;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Returns subscriptions stored in {@link SharedPreferences}.
+ *
+ * @param context used for accessing shared preferences.
+ * @return a list of subscriptions or empty list if none exist.
+ */
+ public static List<Subscription> getSubscriptions(Context context) {
+ return SharedPreferencesHelper.readSubscriptions(context);
+ }
+
+ /**
+ * Finds a subscription given a channel id that the subscription is associated with.
+ *
+ * @param context used for accessing shared preferences.
+ * @param channelId of the channel that the subscription is associated with.
+ * @return a subscription or null if none exist.
+ */
+ @Nullable
+ public static Subscription findSubscriptionByChannelId(Context context, long channelId) {
+ for (Subscription subscription : getSubscriptions(context)) {
+ if (subscription.getChannelId() == channelId) {
+ return subscription;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Finds a subscription with the given name.
+ *
+ * @param context used for accessing shared preferences.
+ * @param name of the subscription.
+ * @return a subscription or null if none exist.
+ */
+ @Nullable
+ public static Subscription findSubscriptionByName(Context context, String name) {
+ for (Subscription subscription : getSubscriptions(context)) {
+ if (subscription.getName().equals(name)) {
+ return subscription;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Overrides the movies stored in {@link SharedPreferences} for a given subscription.
+ *
+ * @param context used for accessing shared preferences.
+ * @param channelId of the channel that the movies are associated with.
+ * @param movies to be stored.
+ */
+ public static void saveMovies(Context context, long channelId, List<Movie> movies) {
+ SharedPreferencesHelper.storeMovies(context, channelId, movies);
+ }
+
+ /**
+ * Removes the list of movies associated with a channel. Overrides the current list with an
+ * empty list in {@link SharedPreferences}.
+ *
+ * @param context used for accessing shared preferences.
+ * @param channelId of the channel that the movies are associated with.
+ */
+ public static void removeMovies(Context context, long channelId) {
+ saveMovies(context, channelId, Collections.<Movie>emptyList());
+ }
+
+ /**
+ * Finds movie in subscriptions with channel id and updates it. Otherwise will add the new movie
+ * to the subscription.
+ *
+ * @param context to access shared preferences.
+ * @param channelId of the subscription that the movie is associated with.
+ * @param movie to be persisted or updated.
+ */
+ public static void saveMovie(Context context, long channelId, Movie movie) {
+ List<Movie> movies = getMovies(context, channelId);
+ int index = findMovie(movies, movie);
+ if (index == -1) {
+ movies.add(movie);
+ } else {
+ movies.set(index, movie);
+ }
+ saveMovies(context, channelId, movies);
+ }
+
+ private static int findMovie(List<Movie> movies, Movie movie) {
+ for (int index = 0; index < movies.size(); ++index) {
+ Movie current = movies.get(index);
+ if (current.getId() == movie.getId()) {
+ return index;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Returns movies stored in {@link SharedPreferences} for a given subscription.
+ *
+ * @param context used for accessing shared preferences.
+ * @param channelId of the subscription that the movie is associated with.
+ * @return a list of movies for a subscription
+ */
+ public static List<Movie> getMovies(Context context, long channelId) {
+ return SharedPreferencesHelper.readMovies(context, channelId);
+ }
+
+ /**
+ * Finds a movie in a subscription by its id.
+ *
+ * @param context to access shared preferences.
+ * @param channelId of the subscription that the movie is associated with.
+ * @param movieId of the movie.
+ * @return a movie or null if none exist.
+ */
+ @Nullable
+ public static Movie findMovieById(Context context, long channelId, long movieId) {
+ for (Movie movie : getMovies(context, channelId)) {
+ if (movie.getId() == movieId) {
+ return movie;
+ }
+ }
+ return null;
+ }
+}
diff --git a/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/model/MockMovieService.java b/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/model/MockMovieService.java
new file mode 100644
index 00000000..53b9d19a
--- /dev/null
+++ b/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/model/MockMovieService.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2017 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.tv.channelsprograms.model;
+
+import android.content.Context;
+
+import com.example.android.tv.channelsprograms.R;
+import com.example.android.tv.channelsprograms.util.AppLinkHelper;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/** Mocks gathering movies from an external source. */
+public final class MockMovieService {
+
+ private static List<Movie> list;
+ private static long count = 0;
+
+ /**
+ * Creates a list of subscriptions that every users should have.
+ *
+ * @param context used for accessing shared preferences.
+ * @return a list of default subscriptions.
+ */
+ public static List<Subscription> createUniversalSubscriptions(Context context) {
+
+ String newForYou = context.getString(R.string.new_for_you);
+ Subscription flagshipSubscription =
+ Subscription.createSubscription(
+ newForYou,
+ context.getString(R.string.new_for_you_description),
+ AppLinkHelper.buildBrowseUri(newForYou).toString(),
+ R.drawable.ic_movie_blue_80dp);
+
+ String trendingVideos = context.getString(R.string.trending_videos);
+ Subscription videoSubscription =
+ Subscription.createSubscription(
+ trendingVideos,
+ context.getString(R.string.trending_videos_description),
+ AppLinkHelper.buildBrowseUri(trendingVideos).toString(),
+ R.drawable.ic_movie_blue_80dp);
+
+ String featuredFilms = context.getString(R.string.featured_films);
+ Subscription filmsSubscription =
+ Subscription.createSubscription(
+ featuredFilms,
+ context.getString(R.string.featured_films_description),
+ AppLinkHelper.buildBrowseUri(featuredFilms).toString(),
+ R.drawable.ic_video_library_blue_80dp);
+
+ return Arrays.asList(flagshipSubscription, videoSubscription, filmsSubscription);
+ }
+
+ /**
+ * Creates and caches a list of movies.
+ *
+ * @return a list of movies.
+ */
+ public static List<Movie> getList() {
+ if (list == null || list.isEmpty()) {
+ list = createMovieList();
+ }
+ return list;
+ }
+
+ /**
+ * Shuffles the list of movies to make the returned list appear to be a different list from
+ * {@link #getList()}.
+ *
+ * @return a list of movies in random order.
+ */
+ public static List<Movie> getFreshList() {
+ List<Movie> shuffledMovies = new ArrayList<>(getList());
+ Collections.shuffle(shuffledMovies);
+ return shuffledMovies;
+ }
+
+ private static List<Movie> createMovieList() {
+ List<Movie> list = new ArrayList<>();
+ String title[] = {
+ "Zeitgeist 2010_ Year in Review",
+ "Google Demo Slam_ 20ft Search",
+ "Introducing Gmail Blue",
+ "Introducing Google Fiber to the Pole",
+ "Introducing Google Nose"
+ };
+
+ String description =
+ "Fusce id nisi turpis. Praesent viverra bibendum semper. "
+ + "Donec tristique, orci sed semper lacinia, quam erat rhoncus massa, non congue tellus est "
+ + "quis tellus. Sed mollis orci venenatis quam scelerisque accumsan. Curabitur a massa sit "
+ + "amet mi accumsan mollis sed et magna. Vivamus sed aliquam risus. Nulla eget dolor in elit "
+ + "facilisis mattis. Ut aliquet luctus lacus. Phasellus nec commodo erat. Praesent tempus id "
+ + "lectus ac scelerisque. Maecenas pretium cursus lectus id volutpat.";
+
+ String studio[] = {
+ "Studio Zero", "Studio One", "Studio Two", "Studio Three", "Studio Four"
+ };
+ String videoUrl[] = {
+ "http://commondatastorage.googleapis.com/android-tv/Sample%20videos/Zeitgeist/Zeitgeist%202010_%20Year%20in%20Review.mp4",
+ "http://commondatastorage.googleapis.com/android-tv/Sample%20videos/Demo%20Slam/Google%20Demo%20Slam_%2020ft%20Search.mp4",
+ "http://commondatastorage.googleapis.com/android-tv/Sample%20videos/April%20Fool's%202013/Introducing%20Gmail%20Blue.mp4",
+ "http://commondatastorage.googleapis.com/android-tv/Sample%20videos/April%20Fool's%202013/Introducing%20Google%20Fiber%20to%20the%20Pole.mp4",
+ "http://commondatastorage.googleapis.com/android-tv/Sample%20videos/April%20Fool's%202013/Introducing%20Google%20Nose.mp4"
+ };
+ String bgImageUrl[] = {
+ "http://commondatastorage.googleapis.com/android-tv/Sample%20videos/Zeitgeist/Zeitgeist%202010_%20Year%20in%20Review/bg.jpg",
+ "http://commondatastorage.googleapis.com/android-tv/Sample%20videos/Demo%20Slam/Google%20Demo%20Slam_%2020ft%20Search/bg.jpg",
+ "http://commondatastorage.googleapis.com/android-tv/Sample%20videos/April%20Fool's%202013/Introducing%20Gmail%20Blue/bg.jpg",
+ "http://commondatastorage.googleapis.com/android-tv/Sample%20videos/April%20Fool's%202013/Introducing%20Google%20Fiber%20to%20the%20Pole/bg.jpg",
+ "http://commondatastorage.googleapis.com/android-tv/Sample%20videos/April%20Fool's%202013/Introducing%20Google%20Nose/bg.jpg",
+ };
+ String cardImageUrl[] = {
+ "http://commondatastorage.googleapis.com/android-tv/Sample%20videos/Zeitgeist/Zeitgeist%202010_%20Year%20in%20Review/card.jpg",
+ "http://commondatastorage.googleapis.com/android-tv/Sample%20videos/Demo%20Slam/Google%20Demo%20Slam_%2020ft%20Search/card.jpg",
+ "http://commondatastorage.googleapis.com/android-tv/Sample%20videos/April%20Fool's%202013/Introducing%20Gmail%20Blue/card.jpg",
+ "http://commondatastorage.googleapis.com/android-tv/Sample%20videos/April%20Fool's%202013/Introducing%20Google%20Fiber%20to%20the%20Pole/card.jpg",
+ "http://commondatastorage.googleapis.com/android-tv/Sample%20videos/April%20Fool's%202013/Introducing%20Google%20Nose/card.jpg"
+ };
+
+ for (int index = 0; index < title.length; ++index) {
+ list.add(
+ buildMovieInfo(
+ "category",
+ title[index],
+ description,
+ studio[index],
+ videoUrl[index],
+ cardImageUrl[index],
+ bgImageUrl[index]));
+ }
+
+ return list;
+ }
+
+ private static Movie buildMovieInfo(
+ String category,
+ String title,
+ String description,
+ String studio,
+ String videoUrl,
+ String cardImageUrl,
+ String backgroundImageUrl) {
+ Movie movie = new Movie();
+ movie.setId(count);
+ incCount();
+ movie.setTitle(title);
+ movie.setDescription(description);
+ movie.setStudio(studio);
+ movie.setCategory(category);
+ movie.setCardImageUrl(cardImageUrl);
+ movie.setBackgroundImageUrl(backgroundImageUrl);
+ movie.setVideoUrl(videoUrl);
+ return movie;
+ }
+
+ private static void incCount() {
+ count++;
+ }
+}
diff --git a/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/model/Movie.java b/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/model/Movie.java
new file mode 100644
index 00000000..ea59c572
--- /dev/null
+++ b/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/model/Movie.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2017 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.tv.channelsprograms.model;
+
+import java.io.Serializable;
+
+/** Represents a video entity with title, description, image thumbs and video url. */
+public class Movie implements Serializable {
+
+ private static final String TAG = "Movie";
+
+ static final long serialVersionUID = 727566175075960653L;
+ private long id;
+ private String title;
+ private String description;
+ private String bgImageUrl;
+ private String cardImageUrl;
+ private String videoUrl;
+ private String studio;
+ private String category;
+ // Program id / Watch Next id returned from the TV Provider.
+ private long programId;
+ private long watchNextId;
+
+ public Movie() {}
+
+ public long getProgramId() {
+ return programId;
+ }
+
+ public void setProgramId(long programId) {
+ this.programId = programId;
+ }
+
+ public long getWatchNextId() {
+ return watchNextId;
+ }
+
+ public void setWatchNextId(long watchNextId) {
+ this.watchNextId = watchNextId;
+ }
+
+ 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;
+ }
+
+ @Override
+ public String toString() {
+ return "Movie{"
+ + "id="
+ + id
+ + ", programId='"
+ + programId
+ + '\''
+ + ", watchNextId='"
+ + watchNextId
+ + '\''
+ + ", title='"
+ + title
+ + '\''
+ + ", videoUrl='"
+ + videoUrl
+ + '\''
+ + ", backgroundImageUrl='"
+ + bgImageUrl
+ + '\''
+ + ", cardImageUrl='"
+ + cardImageUrl
+ + '\''
+ + '}';
+ }
+}
diff --git a/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/model/Subscription.java b/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/model/Subscription.java
new file mode 100644
index 00000000..ad8f558b
--- /dev/null
+++ b/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/model/Subscription.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2017 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.tv.channelsprograms.model;
+
+/** Contains the data about a channel that will be displayed on the launcher. */
+public class Subscription {
+
+ private long channelId;
+ private String name;
+ private String description;
+ private String appLinkIntentUri;
+ private int channelLogo;
+
+ /** Constructor for Gson to use. */
+ public Subscription() {}
+
+ private Subscription(
+ String name, String description, String appLinkIntentUri, int channelLogo) {
+ this.name = name;
+ this.description = description;
+ this.appLinkIntentUri = appLinkIntentUri;
+ this.channelLogo = channelLogo;
+ }
+
+ public static Subscription createSubscription(
+ String name, String description, String appLinkIntentUri, int channelLogo) {
+ return new Subscription(name, description, appLinkIntentUri, channelLogo);
+ }
+
+ public long getChannelId() {
+ return channelId;
+ }
+
+ public void setChannelId(long channelId) {
+ this.channelId = channelId;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getAppLinkIntentUri() {
+ return appLinkIntentUri;
+ }
+
+ public void setAppLinkIntentUri(String appLinkIntentUri) {
+ this.appLinkIntentUri = appLinkIntentUri;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public int getChannelLogo() {
+ return channelLogo;
+ }
+
+ public void setChannelLogo(int channelLogo) {
+ this.channelLogo = channelLogo;
+ }
+}
diff --git a/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/playback/PlaybackActivity.java b/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/playback/PlaybackActivity.java
new file mode 100644
index 00000000..f08e931b
--- /dev/null
+++ b/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/playback/PlaybackActivity.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2017 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.tv.channelsprograms.playback;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+import com.example.android.tv.channelsprograms.R;
+
+/** Loads {@link PlaybackVideoFragment}. */
+public class PlaybackActivity extends Activity {
+
+ public static final String EXTRA_MOVIE = "com.example.android.tv.recommendations.extra.MOVIE";
+ public static final String EXTRA_CHANNEL_ID =
+ "com.example.android.tv.recommendations.extra.CHANNEL_ID";
+ public static final String EXTRA_POSITION =
+ "com.example.android.tv.recommendations.extra.POSITION";
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.playback_controls);
+ }
+}
diff --git a/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/playback/PlaybackVideoFragment.java b/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/playback/PlaybackVideoFragment.java
new file mode 100644
index 00000000..560b2d45
--- /dev/null
+++ b/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/playback/PlaybackVideoFragment.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2017 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.tv.channelsprograms.playback;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v17.leanback.app.VideoFragment;
+import android.support.v17.leanback.app.VideoFragmentGlueHost;
+import android.support.v17.leanback.media.MediaPlayerAdapter;
+import android.support.v17.leanback.media.PlaybackGlue;
+import android.support.v17.leanback.widget.PlaybackControlsRow;
+import android.util.Log;
+
+import com.example.android.tv.channelsprograms.model.Movie;
+
+/** Handles video playback with media controls. */
+public class PlaybackVideoFragment extends VideoFragment {
+
+ private static final String TAG = "VideoFragment";
+
+ private SimplePlaybackTransportControlGlue<MediaPlayerAdapter> mMediaPlayerGlue;
+
+ private long mChannelId;
+ private long mStartingPosition;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mChannelId = getActivity().getIntent().getLongExtra(PlaybackActivity.EXTRA_CHANNEL_ID, -1L);
+ mStartingPosition =
+ getActivity().getIntent().getLongExtra(PlaybackActivity.EXTRA_POSITION, -1L);
+ final Movie movie =
+ (Movie)
+ getActivity()
+ .getIntent()
+ .getSerializableExtra(PlaybackActivity.EXTRA_MOVIE);
+
+ VideoFragmentGlueHost glueHost = new VideoFragmentGlueHost(PlaybackVideoFragment.this);
+
+ mMediaPlayerGlue =
+ new SimplePlaybackTransportControlGlue<>(
+ getActivity(), new MediaPlayerAdapter(getActivity()));
+ mMediaPlayerGlue.setHost(glueHost);
+ mMediaPlayerGlue.setRepeatMode(PlaybackControlsRow.RepeatAction.NONE);
+ mMediaPlayerGlue.addPlayerCallback(
+ new PlaybackGlue.PlayerCallback() {
+ WatchNextAdapter watchNextAdapter = new WatchNextAdapter();
+
+ @Override
+ public void onPlayStateChanged(PlaybackGlue glue) {
+ super.onPlayStateChanged(glue);
+ // TODO: step 10 update progress.
+ long position = mMediaPlayerGlue.getCurrentPosition();
+ long duration = mMediaPlayerGlue.getDuration();
+ watchNextAdapter.updateProgress(
+ getContext(), mChannelId, movie, position, duration);
+ }
+
+ @Override
+ public void onPlayCompleted(PlaybackGlue glue) {
+ super.onPlayCompleted(glue);
+ // TODO: step 11 remove watch next.
+ watchNextAdapter.removeFromWatchNext(
+ getContext(), mChannelId, movie.getId());
+ }
+ });
+
+ mMediaPlayerGlue.setTitle(movie.getTitle());
+ mMediaPlayerGlue.setSubtitle(movie.getDescription());
+ mMediaPlayerGlue.getPlayerAdapter().setDataSource(Uri.parse(movie.getVideoUrl()));
+ seekToStartingPosition();
+ playWhenReady(mMediaPlayerGlue);
+ }
+
+ @Override
+ public void onPause() {
+ if (mMediaPlayerGlue != null) {
+ mMediaPlayerGlue.pause();
+ }
+ super.onPause();
+ }
+
+ private void playWhenReady(final PlaybackGlue glue) {
+ if (glue.isPrepared()) {
+ glue.play();
+ } else {
+ glue.addPlayerCallback(
+ new PlaybackGlue.PlayerCallback() {
+ @Override
+ public void onPreparedStateChanged(PlaybackGlue glue) {
+ if (glue.isPrepared()) {
+ glue.removePlayerCallback(this);
+ glue.play();
+ }
+ }
+ });
+ }
+ }
+
+ private void seekToStartingPosition() {
+ // Skip ahead if given a starting position.
+ if (mStartingPosition > -1L) {
+ if (mMediaPlayerGlue.isPrepared()) {
+ Log.d("VideoFragment", "Is prepped, seeking to " + mStartingPosition);
+ mMediaPlayerGlue.seekTo(mStartingPosition);
+ } else {
+ mMediaPlayerGlue.addPlayerCallback(
+ new PlaybackGlue.PlayerCallback() {
+ @Override
+ public void onPreparedStateChanged(PlaybackGlue glue) {
+ super.onPreparedStateChanged(glue);
+ if (mMediaPlayerGlue.isPrepared()) {
+ mMediaPlayerGlue.removePlayerCallback(this);
+ Log.d(TAG, "In callback, seeking to " + mStartingPosition);
+ mMediaPlayerGlue.seekTo(mStartingPosition);
+ }
+ }
+ });
+ }
+ }
+ }
+}
diff --git a/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/playback/SimplePlaybackTransportControlGlue.java b/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/playback/SimplePlaybackTransportControlGlue.java
new file mode 100644
index 00000000..9f197875
--- /dev/null
+++ b/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/playback/SimplePlaybackTransportControlGlue.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2017 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.tv.channelsprograms.playback;
+
+import android.content.Context;
+import android.os.Handler;
+import android.support.v17.leanback.media.PlaybackTransportControlGlue;
+import android.support.v17.leanback.media.PlayerAdapter;
+import android.support.v17.leanback.widget.Action;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.PlaybackControlsRow;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.Toast;
+
+/**
+ * Handles common primary and secondary actions such as repeat, thumbs up/down, picture in picture,
+ * and closed captions.
+ */
+class SimplePlaybackTransportControlGlue<T extends PlayerAdapter>
+ extends PlaybackTransportControlGlue<T> {
+
+ private PlaybackControlsRow.RepeatAction mRepeatAction;
+ private PlaybackControlsRow.ThumbsUpAction mThumbsUpAction;
+ private PlaybackControlsRow.ThumbsDownAction mThumbsDownAction;
+ private PlaybackControlsRow.PictureInPictureAction mPipAction;
+ private PlaybackControlsRow.ClosedCaptioningAction mClosedCaptioningAction;
+ private Handler mHandler = new Handler();
+
+ public SimplePlaybackTransportControlGlue(Context context, T impl) {
+ super(context, impl);
+ mClosedCaptioningAction = new PlaybackControlsRow.ClosedCaptioningAction(context);
+ mThumbsUpAction = new PlaybackControlsRow.ThumbsUpAction(context);
+ mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsUpAction.OUTLINE);
+ mThumbsDownAction = new PlaybackControlsRow.ThumbsDownAction(context);
+ mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsDownAction.OUTLINE);
+ mRepeatAction = new PlaybackControlsRow.RepeatAction(context);
+ mPipAction = new PlaybackControlsRow.PictureInPictureAction(context);
+ }
+
+ @Override
+ protected void onCreatePrimaryActions(ArrayObjectAdapter adapter) {
+ super.onCreatePrimaryActions(adapter);
+ adapter.add(mRepeatAction);
+ adapter.add(mClosedCaptioningAction);
+ }
+
+ @Override
+ protected void onCreateSecondaryActions(ArrayObjectAdapter adapter) {
+ super.onCreateSecondaryActions(adapter);
+ adapter.add(mThumbsUpAction);
+ adapter.add(mThumbsDownAction);
+ adapter.add(mPipAction);
+ }
+
+ @Override
+ public void onActionClicked(Action action) {
+ if (shouldDispatchAction(action)) {
+ dispatchAction(action, getPrimaryActionsAdapter());
+ dispatchAction(action, getSecondaryActionsAdapter());
+ return;
+ }
+ super.onActionClicked(action);
+ }
+
+ @Override
+ public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
+ if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
+ boolean dispatched = dispatchAction(keyEvent, getPrimaryActionsAdapter());
+ dispatched |= dispatchAction(keyEvent, getSecondaryActionsAdapter());
+ if (dispatched) {
+ return true;
+ }
+ }
+ return super.onKey(view, keyCode, keyEvent);
+ }
+
+ private boolean dispatchAction(KeyEvent keyEvent, ArrayObjectAdapter adapter) {
+ Action action = getControlsRow().getActionForKeyCode(adapter, keyEvent.getKeyCode());
+ if (shouldDispatchAction(action)) {
+ dispatchAction(action, adapter);
+ return true;
+ }
+ return false;
+ }
+
+ private boolean shouldDispatchAction(Action action) {
+ return action == mRepeatAction || action == mThumbsUpAction || action == mThumbsDownAction;
+ }
+
+ private void dispatchAction(Action action, ArrayObjectAdapter adapter) {
+ Toast.makeText(getContext(), action.toString(), Toast.LENGTH_SHORT).show();
+ PlaybackControlsRow.MultiAction multiAction = (PlaybackControlsRow.MultiAction) action;
+ multiAction.nextIndex();
+ notifyActionChanged(multiAction, adapter);
+ }
+
+ private void notifyActionChanged(
+ PlaybackControlsRow.MultiAction action, ArrayObjectAdapter adapter) {
+ if (adapter != null) {
+ int index = adapter.indexOf(action);
+ if (index >= 0) {
+ adapter.notifyArrayItemRangeChanged(index, 1);
+ }
+ }
+ }
+
+ private ArrayObjectAdapter getPrimaryActionsAdapter() {
+ if (getControlsRow() == null) {
+ return null;
+ }
+ return (ArrayObjectAdapter) getControlsRow().getPrimaryActionsAdapter();
+ }
+
+ private ArrayObjectAdapter getSecondaryActionsAdapter() {
+ if (getControlsRow() == null) {
+ return null;
+ }
+ return (ArrayObjectAdapter) getControlsRow().getSecondaryActionsAdapter();
+ }
+
+ @Override
+ protected void onPlayCompleted() {
+ super.onPlayCompleted();
+ mHandler.post(
+ () -> {
+ if (mRepeatAction.getIndex() != PlaybackControlsRow.RepeatAction.INDEX_NONE) {
+ play();
+ }
+ });
+ }
+
+ /**
+ * Sets the behavior for the repeat action. The possible modes are
+ *
+ * <ul>
+ * <li>{@link PlaybackControlsRow.RepeatAction#INDEX_NONE}
+ * <li>{@link PlaybackControlsRow.RepeatAction#INDEX_ALL}
+ * <li>{@link PlaybackControlsRow.RepeatAction#INDEX_ONE}
+ * </ul>
+ *
+ * @param mode for repeat behavior.
+ */
+ public void setRepeatMode(int mode) {
+ mRepeatAction.setIndex(mode);
+ notifyActionChanged(mRepeatAction, getPrimaryActionsAdapter());
+ }
+}
diff --git a/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/playback/WatchNextAdapter.java b/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/playback/WatchNextAdapter.java
new file mode 100644
index 00000000..a06c3e31
--- /dev/null
+++ b/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/playback/WatchNextAdapter.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2017 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.tv.channelsprograms.playback;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.media.tv.TvContractCompat;
+import android.support.media.tv.WatchNextProgram;
+import android.util.Log;
+
+import com.example.android.tv.channelsprograms.model.MockDatabase;
+import com.example.android.tv.channelsprograms.model.Movie;
+import com.example.android.tv.channelsprograms.util.AppLinkHelper;
+
+/** Adds, updates, and removes the currently playing {@link Movie} from the "Watch Next" channel. */
+public class WatchNextAdapter {
+
+ private static final String TAG = "WatchNextAdapter";
+
+ public void updateProgress(
+ Context context, long channelId, Movie movie, long position, long duration) {
+ Log.d(TAG, String.format("Updating the movie (%d) in watch next.", movie.getId()));
+
+ Movie entity = MockDatabase.findMovieById(context, channelId, movie.getId());
+ if (entity == null) {
+ Log.e(
+ TAG,
+ String.format(
+ "Could not find movie in channel: channel id: %d, movie id: %d",
+ channelId, movie.getId()));
+ return;
+ }
+
+ // TODO: step 12 add watch next program.
+ WatchNextProgram program = createWatchNextProgram(channelId, entity, position, duration);
+ if (entity.getWatchNextId() < 1L) {
+ // Create a program.
+ Uri watchNextProgramUri =
+ context.getContentResolver()
+ .insert(
+ TvContractCompat.WatchNextPrograms.CONTENT_URI,
+ program.toContentValues());
+ long watchNextId = ContentUris.parseId(watchNextProgramUri);
+ entity.setWatchNextId(watchNextId);
+ MockDatabase.saveMovie(context, channelId, entity);
+
+ Log.d(TAG, "Watch Next program added: " + watchNextId);
+ } else {
+ // TODO: step 14 update program.
+ // Updates the progress and last engagement time of the program.
+ context.getContentResolver()
+ .update(
+ TvContractCompat.buildWatchNextProgramUri(entity.getWatchNextId()),
+ program.toContentValues(),
+ null,
+ null);
+
+ Log.d(TAG, "Watch Next program updated: " + entity.getWatchNextId());
+ }
+ }
+
+ @NonNull
+ private WatchNextProgram createWatchNextProgram(
+ long channelId, Movie movie, long position, long duration) {
+ // TODO: step 13 convert movie
+ Uri posterArtUri = Uri.parse(movie.getCardImageUrl());
+ Uri intentUri = AppLinkHelper.buildPlaybackUri(channelId, movie.getId(), position);
+
+ WatchNextProgram.Builder builder = new WatchNextProgram.Builder();
+ builder.setType(TvContractCompat.PreviewProgramColumns.TYPE_MOVIE)
+ .setWatchNextType(TvContractCompat.WatchNextPrograms.WATCH_NEXT_TYPE_CONTINUE)
+ .setLastEngagementTimeUtcMillis(System.currentTimeMillis())
+ .setLastPlaybackPositionMillis((int) position)
+ .setDurationMillis((int) duration)
+ .setTitle(movie.getTitle())
+ .setDescription(movie.getDescription())
+ .setPosterArtUri(posterArtUri)
+ .setIntentUri(intentUri);
+ return builder.build();
+ }
+
+ public void removeFromWatchNext(Context context, long channelId, long movieId) {
+ Movie movie = MockDatabase.findMovieById(context, channelId, movieId);
+ if (movie == null || movie.getWatchNextId() < 1L) {
+ Log.d(TAG, "No program to remove from watch next.");
+ return;
+ }
+
+ // TODO: step 15 remove program
+ int rows =
+ context.getContentResolver()
+ .delete(
+ TvContractCompat.buildWatchNextProgramUri(movie.getWatchNextId()),
+ null,
+ null);
+ Log.d(TAG, String.format("Deleted %d programs(s) from watch next", rows));
+
+ // Sync our records with the system; remove reference to watch next program.
+ movie.setWatchNextId(-1);
+ MockDatabase.saveMovie(context, channelId, movie);
+ }
+}
diff --git a/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/util/AppLinkHelper.java b/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/util/AppLinkHelper.java
new file mode 100644
index 00000000..a530cdad
--- /dev/null
+++ b/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/util/AppLinkHelper.java
@@ -0,0 +1,215 @@
+package com.example.android.tv.channelsprograms.util;
+
+import android.net.Uri;
+import android.support.annotation.StringDef;
+
+import java.util.List;
+
+/** Builds and parses uris for deep linking within the app. */
+public class AppLinkHelper {
+
+ private static final String SCHEMA_URI_PREFIX = "tvrecommendation://app/";
+ public static final String PLAYBACK = "playback";
+ public static final String BROWSE = "browse";
+ private static final String URI_PLAY = SCHEMA_URI_PREFIX + PLAYBACK;
+ private static final String URI_VIEW = SCHEMA_URI_PREFIX + BROWSE;
+ private static final int URI_INDEX_OPTION = 0;
+ private static final int URI_INDEX_CHANNEL = 1;
+ private static final int URI_INDEX_MOVIE = 2;
+ private static final int URI_INDEX_POSITION = 3;
+ public static final int DEFAULT_POSITION = -1;
+
+ /**
+ * Builds a {@link Uri} for deep link into playing a movie from the beginning.
+ *
+ * @param channelId - id of the channel the movie is in.
+ * @param movieId - id of the movie.
+ * @return a uri.
+ */
+ public static Uri buildPlaybackUri(long channelId, long movieId) {
+ return buildPlaybackUri(channelId, movieId, DEFAULT_POSITION);
+ }
+
+ /**
+ * Builds a {@link Uri} to deep link into continue playing a movie from a position.
+ *
+ * @param channelId - id of the channel the movie is in.
+ * @param movieId - id of the movie.
+ * @param position - position to continue playing.
+ * @return a uri.
+ */
+ public static Uri buildPlaybackUri(long channelId, long movieId, long position) {
+ return Uri.parse(URI_PLAY)
+ .buildUpon()
+ .appendPath(String.valueOf(channelId))
+ .appendPath(String.valueOf(movieId))
+ .appendPath(String.valueOf(position))
+ .build();
+ }
+
+ /**
+ * Builds a {@link Uri} to deep link into viewing a subscription.
+ *
+ * @param subscriptionName - name of the subscription.
+ * @return a uri.
+ */
+ public static Uri buildBrowseUri(String subscriptionName) {
+ return Uri.parse(URI_VIEW).buildUpon().appendPath(subscriptionName).build();
+ }
+
+ /**
+ * Returns an {@link AppLinkAction} for the given Uri.
+ *
+ * @param uri to determine the intended action.
+ * @return an action.
+ */
+ public static AppLinkAction extractAction(Uri uri) {
+ if (isPlaybackUri(uri)) {
+ return new PlaybackAction(
+ extractChannelId(uri), extractMovieId(uri), extractPosition(uri));
+ } else if (isBrowseUri(uri)) {
+ return new BrowseAction(extractSubscriptionName(uri));
+ }
+ throw new IllegalArgumentException("No action found for uri " + uri);
+ }
+
+ /**
+ * Tests if the {@link Uri} was built for playing a movie.
+ *
+ * @param uri to examine.
+ * @return true if the uri is for playing a movie.
+ */
+ private static boolean isPlaybackUri(Uri uri) {
+ if (uri.getPathSegments().isEmpty()) {
+ return false;
+ }
+ String option = uri.getPathSegments().get(URI_INDEX_OPTION);
+ return PLAYBACK.equals(option);
+ }
+
+ /**
+ * Tests if a {@link Uri} was built for browsing a subscription.
+ *
+ * @param uri to examine.
+ * @return true if the Uri is for browsing a subscription.
+ */
+ private static boolean isBrowseUri(Uri uri) {
+ if (uri.getPathSegments().isEmpty()) {
+ return false;
+ }
+ String option = uri.getPathSegments().get(URI_INDEX_OPTION);
+ return BROWSE.equals(option);
+ }
+
+ /**
+ * Extracts the subscription name from the {@link Uri}.
+ *
+ * @param uri that contains a subscription name.
+ * @return the subscription name.
+ */
+ private static String extractSubscriptionName(Uri uri) {
+ return extract(uri, URI_INDEX_CHANNEL);
+ }
+
+ /**
+ * Extracts the channel id from the {@link Uri}.
+ *
+ * @param uri that contains a channel id.
+ * @return the channel id.
+ */
+ private static long extractChannelId(Uri uri) {
+ return extractLong(uri, URI_INDEX_CHANNEL);
+ }
+
+ /**
+ * Extracts the movie id from the {@link Uri}.
+ *
+ * @param uri that contains a movie id.
+ * @return the movie id.
+ */
+ private static long extractMovieId(Uri uri) {
+ return extractLong(uri, URI_INDEX_MOVIE);
+ }
+
+ /**
+ * Extracts the playback mPosition from the {@link Uri}.
+ *
+ * @param uri that contains a playback mPosition.
+ * @return the playback mPosition.
+ */
+ private static long extractPosition(Uri uri) {
+ return extractLong(uri, URI_INDEX_POSITION);
+ }
+
+ private static long extractLong(Uri uri, int index) {
+ return Long.valueOf(extract(uri, index));
+ }
+
+ private static String extract(Uri uri, int index) {
+ List<String> pathSegments = uri.getPathSegments();
+ if (pathSegments.isEmpty() || pathSegments.size() < index) {
+ return null;
+ }
+ return pathSegments.get(index);
+ }
+
+ @StringDef({BROWSE, PLAYBACK})
+ public @interface ActionFlags {}
+
+ /** Action for deep linking. */
+ public interface AppLinkAction {
+ /** Returns an string representation of the action. */
+ @ActionFlags
+ String getAction();
+ }
+
+ /** Browse a subscription. */
+ public static class BrowseAction implements AppLinkAction {
+
+ private final String mSubscriptionName;
+
+ private BrowseAction(String subscriptionName) {
+ this.mSubscriptionName = subscriptionName;
+ }
+
+ public String getSubscriptionName() {
+ return mSubscriptionName;
+ }
+
+ @Override
+ public String getAction() {
+ return BROWSE;
+ }
+ }
+
+ /** Play a movie. */
+ public static class PlaybackAction implements AppLinkAction {
+
+ private final long mChannelId;
+ private final long mMovieId;
+ private final long mPosition;
+
+ private PlaybackAction(long channelId, long movieId, long position) {
+ this.mChannelId = channelId;
+ this.mMovieId = movieId;
+ this.mPosition = position;
+ }
+
+ public long getChannelId() {
+ return mChannelId;
+ }
+
+ public long getMovieId() {
+ return mMovieId;
+ }
+
+ public long getPosition() {
+ return mPosition;
+ }
+
+ @Override
+ public String getAction() {
+ return PLAYBACK;
+ }
+ }
+}
diff --git a/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/util/SharedPreferencesHelper.java b/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/util/SharedPreferencesHelper.java
new file mode 100644
index 00000000..79a0341c
--- /dev/null
+++ b/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/util/SharedPreferencesHelper.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (c) 2017 Google Inc.
+ *
+ * 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.tv.channelsprograms.util;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+import com.example.android.tv.channelsprograms.model.Movie;
+import com.example.android.tv.channelsprograms.model.Subscription;
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Helper class to store {@link Subscription}s and {@link Movie}s in {@link SharedPreferences}.
+ *
+ * <p>SharedPreferencesHelper provides static methods to set and get these objects.
+ *
+ * <p>The methods of this class should not be called on the UI thread. Marshalling an object into
+ * JSON can be expensive for large objects.
+ */
+public final class SharedPreferencesHelper {
+
+ private static final String TAG = "SharedPreferencesHelper";
+
+ private static final String PREFS_NAME = "com.example.android.tv.recommendations";
+ private static final String PREFS_SUBSCRIPTIONS_KEY =
+ "com.example.android.tv.recommendations.prefs.SUBSCRIPTIONS";
+ private static final String PREFS_SUBSCRIBED_MOVIES_PREFIX =
+ "com.example.android.tv.recommendations.prefs.SUBSCRIBED_MOVIES_";
+
+ private static final Gson mGson = new Gson();
+
+ /**
+ * Reads the {@link List <Subscription>} from {@link SharedPreferences}.
+ *
+ * @param context used for getting an instance of shared preferences.
+ * @return a list of subscriptions or an empty list if none exist.
+ */
+ public static List<Subscription> readSubscriptions(Context context) {
+ return getList(context, Subscription.class, PREFS_SUBSCRIPTIONS_KEY);
+ }
+
+ /**
+ * Overrides the subscriptions stored in {@link SharedPreferences}.
+ *
+ * @param context used for getting an instance of shared preferences.
+ * @param subscriptions to be stored in shared preferences.
+ */
+ public static void storeSubscriptions(Context context, List<Subscription> subscriptions) {
+ setList(context, subscriptions, PREFS_SUBSCRIPTIONS_KEY);
+ }
+
+ /**
+ * Reads the {@link List <Movie>} from {@link SharedPreferences} for a given channel.
+ *
+ * @param context used for getting an instance of shared preferences.
+ * @param channelId of the channel that the movies are associated with.
+ * @return a list of movies or an empty list if none exist.
+ */
+ public static List<Movie> readMovies(Context context, long channelId) {
+ return getList(context, Movie.class, PREFS_SUBSCRIBED_MOVIES_PREFIX + channelId);
+ }
+
+ /**
+ * Overrides the movies stored in {@link SharedPreferences} for the associated channel id.
+ *
+ * @param context used for getting an instance of shared preferences.
+ * @param channelId of the channel that the movies are associated with.
+ * @param movies to be stored.
+ */
+ public static void storeMovies(Context context, long channelId, List<Movie> movies) {
+ setList(context, movies, PREFS_SUBSCRIBED_MOVIES_PREFIX + channelId);
+ }
+
+ /**
+ * Retrieves a set of Strings from {@link SharedPreferences} and returns as a List.
+ *
+ * @param context used for getting an instance of shared preferences.
+ * @param clazz the class that the strings will be unmarshalled into.
+ * @param key the key in shared preferences to access the string set.
+ * @param <T> the type of object that will be in the returned list, should be the same as the
+ * clazz that was supplied.
+ * @return a list of <T> objects that were stored in shared preferences or an empty list if no
+ * objects exists.
+ */
+ private static <T> List<T> getList(Context context, Class<T> clazz, String key) {
+ SharedPreferences sharedPreferences =
+ context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
+ Set<String> stringSet = sharedPreferences.getStringSet(key, new HashSet<String>());
+ if (stringSet.isEmpty()) {
+ // Favoring mutability of the list over Collections.emptyList().
+ return new ArrayList<>();
+ }
+ List<T> list = new ArrayList<>(stringSet.size());
+ try {
+ for (String contactString : stringSet) {
+ list.add(mGson.fromJson(contactString, clazz));
+ }
+ } catch (JsonSyntaxException e) {
+ Log.e(TAG, "Could not parse json.", e);
+ return Collections.emptyList();
+ }
+ return list;
+ }
+
+ /**
+ * Saves a list of Strings into {@link SharedPreferences}.
+ *
+ * @param context used for getting an instance of shared preferences.
+ * @param list of <T> object that need to be persisted.
+ * @param key the key in shared preferences which the string set will be stored.
+ * @param <T> type the of object we will be marshalling and persisting.
+ */
+ private static <T> void setList(Context context, List<T> list, String key) {
+ SharedPreferences sharedPreferences =
+ context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+
+ Set<String> strings = new LinkedHashSet<>(list.size());
+ for (T item : list) {
+ strings.add(mGson.toJson(item));
+ }
+ editor.putStringSet(key, strings);
+ editor.apply();
+ }
+}
diff --git a/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/util/TvUtil.java b/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/util/TvUtil.java
new file mode 100644
index 00000000..25bb8a4a
--- /dev/null
+++ b/tv/ChannelsPrograms/Application/src/main/java/com/example/android/tv/channelsprograms/util/TvUtil.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (c) 2017 Google Inc.
+ *
+ * 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.tv.channelsprograms.util;
+
+import android.app.job.JobInfo;
+import android.app.job.JobScheduler;
+import android.content.ComponentName;
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.VectorDrawable;
+import android.media.tv.TvContract;
+import android.net.Uri;
+import android.os.PersistableBundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.WorkerThread;
+import android.support.media.tv.Channel;
+import android.support.media.tv.ChannelLogoUtils;
+import android.support.media.tv.TvContractCompat;
+import android.util.Log;
+
+import com.example.android.tv.channelsprograms.SyncChannelJobService;
+import com.example.android.tv.channelsprograms.SyncProgramsJobService;
+import com.example.android.tv.channelsprograms.model.Subscription;
+
+/** Manages interactions with the TV Provider. */
+public class TvUtil {
+
+ private static final String TAG = "TvUtil";
+ private static final long CHANNEL_JOB_ID_OFFSET = 1000;
+
+ private static final String[] CHANNELS_PROJECTION = {
+ TvContractCompat.Channels._ID,
+ TvContract.Channels.COLUMN_DISPLAY_NAME,
+ TvContractCompat.Channels.COLUMN_BROWSABLE
+ };
+
+ /**
+ * Converts a {@link Subscription} into a {@link Channel} and adds it to the tv provider.
+ *
+ * @param context used for accessing a content resolver.
+ * @param subscription to be converted to a channel and added to the tv provider.
+ * @return the id of the channel that the tv provider returns.
+ */
+ @WorkerThread
+ public static long createChannel(Context context, Subscription subscription) {
+
+ // Checks if our subscription has been added to the channels before.
+ Cursor cursor =
+ context.getContentResolver()
+ .query(
+ TvContractCompat.Channels.CONTENT_URI,
+ CHANNELS_PROJECTION,
+ null,
+ null,
+ null);
+ if (cursor != null && cursor.moveToFirst()) {
+ do {
+ Channel channel = Channel.fromCursor(cursor);
+ if (subscription.getName().equals(channel.getDisplayName())) {
+ Log.d(
+ TAG,
+ "Channel already exists. Returning channel "
+ + channel.getId()
+ + " from TV Provider.");
+ return channel.getId();
+ }
+ } while (cursor.moveToNext());
+ }
+
+ // Create the channel since it has not been added to the TV Provider.
+ Uri appLinkIntentUri = Uri.parse(subscription.getAppLinkIntentUri());
+
+ Channel.Builder builder = new Channel.Builder();
+ builder.setType(TvContractCompat.Channels.TYPE_PREVIEW)
+ .setDisplayName(subscription.getName())
+ .setDescription(subscription.getDescription())
+ .setAppLinkIntentUri(appLinkIntentUri);
+
+ Log.d(TAG, "Creating channel: " + subscription.getName());
+ Uri channelUrl =
+ context.getContentResolver()
+ .insert(
+ TvContractCompat.Channels.CONTENT_URI,
+ builder.build().toContentValues());
+
+ Log.d(TAG, "channel insert at " + channelUrl);
+ long channelId = ContentUris.parseId(channelUrl);
+ Log.d(TAG, "channel id " + channelId);
+
+ Bitmap bitmap = convertToBitmap(context, subscription.getChannelLogo());
+ ChannelLogoUtils.storeChannelLogo(context, channelId, bitmap);
+
+ return channelId;
+ }
+
+ public static int getNumberOfChannels(Context context) {
+ Cursor cursor =
+ context.getContentResolver()
+ .query(
+ TvContractCompat.Channels.CONTENT_URI,
+ CHANNELS_PROJECTION,
+ null,
+ null,
+ null);
+ return cursor != null ? cursor.getCount() : 0;
+ }
+
+ /**
+ * Converts a resource into a {@link Bitmap}. If the resource is a vector drawable, it will be
+ * drawn into a new Bitmap. Otherwise the {@link BitmapFactory} will decode the resource.
+ *
+ * @param context used for getting the drawable from resources.
+ * @param resourceId of the drawable.
+ * @return a bitmap of the resource.
+ */
+ @NonNull
+ public static Bitmap convertToBitmap(Context context, int resourceId) {
+ Drawable drawable = context.getDrawable(resourceId);
+ if (drawable instanceof VectorDrawable) {
+ Bitmap 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;
+ }
+
+ return BitmapFactory.decodeResource(context.getResources(), resourceId);
+ }
+
+ /**
+ * Schedules syncing channels via a {@link JobScheduler}.
+ *
+ * @param context for accessing the {@link JobScheduler}.
+ */
+ public static void scheduleSyncingChannel(Context context) {
+ ComponentName componentName = new ComponentName(context, SyncChannelJobService.class);
+ JobInfo.Builder builder = new JobInfo.Builder(1, componentName);
+ builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
+
+ JobScheduler scheduler =
+ (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
+
+ Log.d(TAG, "Scheduled channel creation.");
+ scheduler.schedule(builder.build());
+ }
+
+ /**
+ * Schedulers syncing programs for a channel. The scheduler will listen to a {@link Uri} for a
+ * particular channel.
+ *
+ * @param context for accessing the {@link JobScheduler}.
+ * @param channelId for the channel to listen for changes.
+ */
+ public static void scheduleSyncingProgramsForChannel(Context context, long channelId) {
+ ComponentName componentName = new ComponentName(context, SyncProgramsJobService.class);
+
+ JobInfo.Builder builder =
+ new JobInfo.Builder(getJobIdForChannelId(channelId), componentName);
+
+ JobInfo.TriggerContentUri triggerContentUri =
+ new JobInfo.TriggerContentUri(
+ TvContractCompat.buildChannelUri(channelId),
+ JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS);
+ builder.addTriggerContentUri(triggerContentUri);
+ builder.setTriggerContentMaxDelay(0L);
+ builder.setTriggerContentUpdateDelay(0L);
+
+ PersistableBundle bundle = new PersistableBundle();
+ bundle.putLong(TvContractCompat.EXTRA_CHANNEL_ID, channelId);
+ builder.setExtras(bundle);
+
+ JobScheduler scheduler =
+ (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
+ scheduler.cancel(getJobIdForChannelId(channelId));
+ scheduler.schedule(builder.build());
+ }
+
+ private static int getJobIdForChannelId(long channelId) {
+ return (int) (CHANNEL_JOB_ID_OFFSET + channelId);
+ }
+}
diff --git a/tv/ChannelsPrograms/Application/src/main/res/drawable/app_icon_your_company.png b/tv/ChannelsPrograms/Application/src/main/res/drawable/app_icon_your_company.png
new file mode 100644
index 00000000..0a47b018
--- /dev/null
+++ b/tv/ChannelsPrograms/Application/src/main/res/drawable/app_icon_your_company.png
Binary files differ
diff --git a/tv/ChannelsPrograms/Application/src/main/res/drawable/ic_movie_blue_80dp.xml b/tv/ChannelsPrograms/Application/src/main/res/drawable/ic_movie_blue_80dp.xml
new file mode 100644
index 00000000..1daad9f7
--- /dev/null
+++ b/tv/ChannelsPrograms/Application/src/main/res/drawable/ic_movie_blue_80dp.xml
@@ -0,0 +1,24 @@
+<!--
+ ~ Copyright 2017 Google Inc.
+ ~
+ ~ 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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="80dp"
+ android:height="80dp"
+ android:viewportHeight="24.0"
+ android:viewportWidth="24.0">
+ <path
+ android:fillColor="#FF0096a6"
+ android:pathData="M18,4l2,4h-3l-2,-4h-2l2,4h-3l-2,-4H8l2,4H7L5,4H4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V4h-4z" />
+</vector>
diff --git a/tv/ChannelsPrograms/Application/src/main/res/drawable/ic_video_library_blue_80dp.xml b/tv/ChannelsPrograms/Application/src/main/res/drawable/ic_video_library_blue_80dp.xml
new file mode 100644
index 00000000..f9c0b236
--- /dev/null
+++ b/tv/ChannelsPrograms/Application/src/main/res/drawable/ic_video_library_blue_80dp.xml
@@ -0,0 +1,24 @@
+<!--
+ ~ Copyright 2017 Google Inc.
+ ~
+ ~ 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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="80dp"
+ android:height="80dp"
+ android:viewportHeight="24.0"
+ android:viewportWidth="24.0">
+ <path
+ android:fillColor="#FF0096a6"
+ android:pathData="M4,6L2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6zM20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM12,14.5v-9l6,4.5 -6,4.5z" />
+</vector>
diff --git a/tv/ChannelsPrograms/Application/src/main/res/layout/activity_main.xml b/tv/ChannelsPrograms/Application/src/main/res/layout/activity_main.xml
new file mode 100644
index 00000000..ed74d0c1
--- /dev/null
+++ b/tv/ChannelsPrograms/Application/src/main/res/layout/activity_main.xml
@@ -0,0 +1,139 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright 2017 Google Inc.
+ ~
+ ~ 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.
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginBottom="27dp"
+ android:layout_marginLeft="48dp"
+ android:layout_marginRight="48dp"
+ android:layout_marginTop="27dp"
+ android:orientation="vertical">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/button_instructions" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="27dp"
+ android:paddingBottom="27dp">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/tv_shows"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+
+ <Button
+ android:id="@+id/subscribe_tv_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/subscribe" />
+
+ </LinearLayout>
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:paddingStart="27dp"
+ android:text="@string/implement_movie_presenter_here"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="27dp">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/video_clips"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+
+ <Button
+ android:id="@+id/subscribe_video_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/subscribe" />
+
+ </LinearLayout>
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:paddingStart="27dp"
+ android:text="@string/implement_movie_presenter_here"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="27dp">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/cat_videos"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+
+ <Button
+ android:id="@+id/subscribe_cat_videos_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/subscribe" />
+
+ </LinearLayout>
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:paddingStart="27dp"
+ android:text="@string/implement_movie_presenter_here"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+
+ </LinearLayout>
+
+ </LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/tv/ChannelsPrograms/Application/src/main/res/layout/playback_controls.xml b/tv/ChannelsPrograms/Application/src/main/res/layout/playback_controls.xml
new file mode 100644
index 00000000..e093a249
--- /dev/null
+++ b/tv/ChannelsPrograms/Application/src/main/res/layout/playback_controls.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright 2017 Google Inc.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <fragment
+ android:id="@+id/playback_controls_fragment"
+ android:name="com.example.android.tv.channelsprograms.playback.PlaybackVideoFragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+</FrameLayout>
diff --git a/tv/ChannelsPrograms/Application/src/main/res/mipmap-hdpi/ic_launcher.png b/tv/ChannelsPrograms/Application/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000..cde69bcc
--- /dev/null
+++ b/tv/ChannelsPrograms/Application/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/tv/ChannelsPrograms/Application/src/main/res/mipmap-mdpi/ic_launcher.png b/tv/ChannelsPrograms/Application/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000..c133a0cb
--- /dev/null
+++ b/tv/ChannelsPrograms/Application/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/tv/ChannelsPrograms/Application/src/main/res/mipmap-xhdpi/ic_launcher.png b/tv/ChannelsPrograms/Application/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..bfa42f0e
--- /dev/null
+++ b/tv/ChannelsPrograms/Application/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/tv/ChannelsPrograms/Application/src/main/res/mipmap-xxhdpi/ic_launcher.png b/tv/ChannelsPrograms/Application/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..324e72cd
--- /dev/null
+++ b/tv/ChannelsPrograms/Application/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/tv/ChannelsPrograms/Application/src/main/res/values/strings.xml b/tv/ChannelsPrograms/Application/src/main/res/values/strings.xml
new file mode 100644
index 00000000..7eb732c0
--- /dev/null
+++ b/tv/ChannelsPrograms/Application/src/main/res/values/strings.xml
@@ -0,0 +1,39 @@
+<!--
+ ~ Copyright 2017 Google Inc.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<resources>
+
+ <string name="channel_added">Channel added</string>
+ <string name="channel_not_added">Channel not added</string>
+
+ <string name="title_tv_shows">Your TV Shows</string>
+ <string name="your_videos">Your Videos</string>
+ <string name="cat_videos">Cat Videos</string>
+ <string name="new_for_you">New for You</string>
+ <string name="new_for_you_description">These are recommended programs that we think you will like.</string>
+ <string name="trending_videos">Trending Videos</string>
+ <string name="trending_videos_description">These are trending videos from our app.</string>
+ <string name="featured_films">Featured Films</string>
+ <string name="featured_films_description">These are featured films that have been rated highly from our staff.</string>
+ <string name="tv_shows">TV Shows</string>
+ <string name="subscribe">Subscribe</string>
+ <string name="video_clips">Video Clips</string>
+ <string name="tv_shows_description">These are TV shows that you have added to your TV Show List.</string>
+ <string name="your_videos_description">These are videos that you have added to your Video List.</string>
+ <string name="cat_videos_description">These are cat videos that you may or may not even like.</string>
+ <string name="button_instructions">Click on the buttons to see how to prompt users for dynamic channels.</string>
+ <string name="implement_movie_presenter_here">Implement movie presenter here.</string>
+
+</resources>
diff --git a/tv/ChannelsPrograms/build.gradle b/tv/ChannelsPrograms/build.gradle
new file mode 100644
index 00000000..9b6a9ce4
--- /dev/null
+++ b/tv/ChannelsPrograms/build.gradle
@@ -0,0 +1,12 @@
+
+
+// BEGIN_EXCLUDE
+import com.example.android.samples.build.SampleGenPlugin
+apply plugin: SampleGenPlugin
+
+samplegen {
+ pathToBuild "../../../../build"
+ pathToSamplesCommon "../../common"
+}
+apply from: "../../../../build/build.gradle"
+// END_EXCLUDE
diff --git a/tv/ChannelsPrograms/buildSrc/build.gradle b/tv/ChannelsPrograms/buildSrc/build.gradle
new file mode 100644
index 00000000..d77115d0
--- /dev/null
+++ b/tv/ChannelsPrograms/buildSrc/build.gradle
@@ -0,0 +1,16 @@
+
+repositories {
+ jcenter()
+}
+dependencies {
+ compile 'org.freemarker:freemarker:2.3.20'
+}
+
+sourceSets {
+ main {
+ groovy {
+ srcDir new File(rootDir, "../../../../../build/buildSrc/src/main/groovy")
+ }
+ }
+}
+
diff --git a/tv/ChannelsPrograms/gradle/wrapper/gradle-wrapper.jar b/tv/ChannelsPrograms/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..8c0fb64a
--- /dev/null
+++ b/tv/ChannelsPrograms/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/tv/ChannelsPrograms/gradle/wrapper/gradle-wrapper.properties b/tv/ChannelsPrograms/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..fcc67a34
--- /dev/null
+++ b/tv/ChannelsPrograms/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue Jun 27 11:00:03 PDT 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.0-rc-1-bin.zip
diff --git a/tv/ChannelsPrograms/gradlew b/tv/ChannelsPrograms/gradlew
new file mode 100755
index 00000000..91a7e269
--- /dev/null
+++ b/tv/ChannelsPrograms/gradlew
@@ -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 "$@"
diff --git a/tv/ChannelsPrograms/gradlew.bat b/tv/ChannelsPrograms/gradlew.bat
new file mode 100644
index 00000000..aec99730
--- /dev/null
+++ b/tv/ChannelsPrograms/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/tv/ChannelsPrograms/settings.gradle b/tv/ChannelsPrograms/settings.gradle
new file mode 100644
index 00000000..0a5c310b
--- /dev/null
+++ b/tv/ChannelsPrograms/settings.gradle
@@ -0,0 +1,2 @@
+
+include 'Application'
diff --git a/tv/ChannelsPrograms/template-params.xml b/tv/ChannelsPrograms/template-params.xml
new file mode 100644
index 00000000..f0461131
--- /dev/null
+++ b/tv/ChannelsPrograms/template-params.xml
@@ -0,0 +1,116 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2017 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.
+-->
+
+<sample>
+ <name>Channels / Programs</name>
+ <group>NoGroup</group> <!-- This field will be deprecated in the future
+ and replaced with the "categories" tags below. -->
+ <package>com.example.android.tv.channelsprograms</package>
+
+ <!-- change minSdk if needed-->
+ <minSdk>26</minSdk>
+ <compileSdk>26.0.0-rc2</compileSdk>
+
+ <!-- Include additional dependencies here.-->
+ <dependency>com.android.support:appcompat-v7:26.0.0-beta2</dependency>
+ <dependency>com.android.support:leanback-v17:26.0.0-beta2</dependency>
+ <dependency>com.android.support:support-tv-provider:26.0.0-beta2</dependency>
+ <dependency>com.github.bumptech.glide:glide:3.8.0</dependency>
+ <dependency>com.google.code.gson:gson:2.8.0</dependency>
+
+ <strings>
+ <intro>
+ <![CDATA[
+ Demonstrates how to add channels and programs to the home screen.
+ ]]>
+ </intro>
+ </strings>
+
+ <!-- The basic templates have already been enabled. Uncomment more as desired. -->
+ <template src="base" />
+ <!-- template src="ActivityCards" / -->
+ <!-- template src="FragmentView" / -->
+ <!-- template src="CardStream" / -->
+ <!-- template src="SimpleView" / -->
+ <!--<template src="SingleView" />-->
+
+ <!-- Include common code modules by uncommenting them below. -->
+ <!--<common src="logger" />-->
+ <!-- common src="activities"/ -->
+
+ <metadata>
+ <!-- Values: {DRAFT | PUBLISHED | INTERNAL | DEPRECATED | SUPERCEDED} -->
+ <status>DRAFT</status>
+ <!-- See http://go/sample-categories for details on the next 4 fields. -->
+ <categories>Getting Started, UI</categories>
+ <technologies>Android</technologies>
+ <languages>Java</languages>
+ <solutions>Mobile</solutions>
+ <!-- Values: {BEGINNER | INTERMEDIATE | ADVANCED | EXPERT} -->
+ <!-- Beginner is for "getting started" type content, or essential content.
+ (e.g. "Hello World", activities, intents)
+
+ Intermediate is for content that covers material a beginner doesn't need
+ to know, but that a skilled developer is expected to know.
+ (e.g. services, basic styles and theming, sync adapters)
+
+ Advanced is for highly technical content geared towards experienced developers.
+ (e.g. performance optimizations, custom views, bluetooth)
+
+ Expert is reserved for highly technical or specialized content, and should
+ be used sparingly. (e.g. VPN clients, SELinux, custom instrumentation runners) -->
+ <level>BEGINNER</level>
+ <!-- Dimensions: 512x512, PNG fomrat -->
+ <icon>screenshots/icon-web.png</icon>
+ <!-- Path to screenshots. Use <img> tags for each. -->
+ <screenshots>
+ <img>screenshots/1-main.png</img>
+ <img>screenshots/2-settings.png</img>
+ </screenshots>
+ <!-- List of APIs that this sample should be cross-referenced under. Use <android>
+ for fully-qualified Framework class names ("android:" namespace).
+
+ Use <ext> for custom namespaces, if needed. See "Samples Index API" documentation
+ for more details. -->
+ <api_refs>
+ <android>android.app.ActionBar</android>
+ </api_refs>
+
+ <!-- 1-3 line description of the sample here.
+
+ Avoid simply rearranging the sample's title. What does this sample actually
+ accomplish, and how does it do it? -->
+ <description>
+ Sample demonstrating how to instantiate an ActionBar on Android, define
+ action items, and set an "up" navigation link. Uses the Support Library
+ for compatibility with pre-3.0 devices.
+ </description>
+
+ <!-- Multi-paragraph introduction to sample, from an educational point-of-view.
+ Makrdown formatting allowed. This will be used to generate a mini-article for the
+ sample on DAC. -->
+ <intro>
+ Long intro here.
+
+ Multi-paragraph introduction to sample, from an educational point-of-view.
+ *Makrdown* formatting allowed. See [Markdown Documentation][1]
+ for details.
+
+ [1]: http://daringfireball.net/projects/markdown/syntax
+ </intro>
+ </metadata>
+</sample>