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