Merge "Initial commit." into oc-dev
diff --git a/tv/ChannelsPrograms/Application/.gitignore b/tv/ChannelsPrograms/Application/.gitignore
new file mode 100644
index 0000000..8232fc3
--- /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 0000000..f2fe155
--- /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 0000000..7f94114
--- /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 0000000..7b1d139
--- /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 0000000..7ddb2f9
--- /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 0000000..04f0990
--- /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 0000000..45d4a3e
--- /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 0000000..47fde6a
--- /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 0000000..f540c39
--- /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 0000000..53b9d19
--- /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 0000000..ea59c57
--- /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 0000000..ad8f558
--- /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 0000000..f08e931
--- /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 0000000..560b2d4
--- /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 0000000..9f19787
--- /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 0000000..a06c3e3
--- /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 0000000..a530cda
--- /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 0000000..79a0341
--- /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 0000000..25bb8a4
--- /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 0000000..0a47b01
--- /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 0000000..1daad9f
--- /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 0000000..f9c0b23
--- /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 0000000..ed74d0c
--- /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 0000000..e093a24
--- /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 0000000..cde69bc
--- /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 0000000..c133a0c
--- /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 0000000..bfa42f0
--- /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 0000000..324e72c
--- /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 0000000..7eb732c
--- /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 0000000..9b6a9ce
--- /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 0000000..d77115d
--- /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 0000000..8c0fb64
--- /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 0000000..fcc67a3
--- /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 0000000..91a7e26
--- /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 0000000..aec9973
--- /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 0000000..0a5c310
--- /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 0000000..f046113
--- /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>