aboutsummaryrefslogtreecommitdiff
path: root/src/com/android/tv/recommendation/ChannelPreviewUpdater.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/tv/recommendation/ChannelPreviewUpdater.java')
-rw-r--r--src/com/android/tv/recommendation/ChannelPreviewUpdater.java323
1 files changed, 323 insertions, 0 deletions
diff --git a/src/com/android/tv/recommendation/ChannelPreviewUpdater.java b/src/com/android/tv/recommendation/ChannelPreviewUpdater.java
new file mode 100644
index 00000000..2709ebe1
--- /dev/null
+++ b/src/com/android/tv/recommendation/ChannelPreviewUpdater.java
@@ -0,0 +1,323 @@
+/*
+ * 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.android.tv.recommendation;
+
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.support.annotation.RequiresApi;
+import android.support.media.tv.TvContractCompat;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.tv.ApplicationSingletons;
+import com.android.tv.TvApplication;
+import com.android.tv.data.Channel;
+import com.android.tv.data.PreviewDataManager;
+import com.android.tv.data.PreviewProgramContent;
+import com.android.tv.data.Program;
+import com.android.tv.parental.ParentalControlSettings;
+import com.android.tv.util.Utils;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/** Class for updating the preview programs for {@link Channel}. */
+@RequiresApi(Build.VERSION_CODES.O)
+public class ChannelPreviewUpdater {
+ private static final String TAG = "ChannelPreviewUpdater";
+ // STOPSHIP: set it to false.
+ private static final boolean DEBUG = true;
+
+ private static final int UPATE_PREVIEW_PROGRAMS_JOB_ID = 1000001;
+ private static final long ROUTINE_INTERVAL_MS = TimeUnit.MINUTES.toMillis(10);
+ // The left time of a program should meet the threshold so that it could be recommended.
+ private static final long RECOMMENDATION_THRESHOLD_LEFT_TIME_MS =
+ TimeUnit.MINUTES.toMillis(10);
+ private static final int RECOMMENDATION_THRESHOLD_PROGRESS = 90; // 90%
+ private static final int RECOMMENDATION_COUNT = 6;
+ private static final int MIN_COUNT_TO_ADD_ROW = 4;
+
+ private static ChannelPreviewUpdater sChannelPreviewUpdater;
+
+ /**
+ * Creates and returns the {@link ChannelPreviewUpdater}.
+ */
+ public static ChannelPreviewUpdater getInstance(Context context) {
+ if (sChannelPreviewUpdater == null) {
+ sChannelPreviewUpdater = new ChannelPreviewUpdater(context.getApplicationContext());
+ }
+ return sChannelPreviewUpdater;
+ }
+
+ private final Context mContext;
+ private final Recommender mRecommender;
+ private final PreviewDataManager mPreviewDataManager;
+ private JobService mJobService;
+ private JobParameters mJobParams;
+
+ private final ParentalControlSettings mParentalControlSettings;
+
+ private boolean mNeedUpdateAfterRecommenderReady = false;
+
+ private Recommender.Listener mRecommenderListener = new Recommender.Listener() {
+ @Override
+ public void onRecommenderReady() {
+ if (mNeedUpdateAfterRecommenderReady) {
+ if (DEBUG) Log.d(TAG, "Recommender is ready");
+ updatePreviewDataForChannelsImmediately();
+ mNeedUpdateAfterRecommenderReady = false;
+ }
+ }
+
+ @Override
+ public void onRecommendationChanged() {
+ updatePreviewDataForChannelsImmediately();
+ }
+ };
+
+ private ChannelPreviewUpdater(Context context) {
+ mContext = context;
+ mRecommender = new Recommender(context, mRecommenderListener, true);
+ mRecommender.registerEvaluator(new RandomEvaluator(), 0.1, 0.1);
+ mRecommender.registerEvaluator(new FavoriteChannelEvaluator(), 0.5, 0.5);
+ mRecommender.registerEvaluator(new RoutineWatchEvaluator(), 1.0, 1.0);
+ ApplicationSingletons appSingleton = TvApplication.getSingletons(context);
+ mPreviewDataManager = appSingleton.getPreviewDataManager();
+ mParentalControlSettings = appSingleton.getTvInputManagerHelper()
+ .getParentalControlSettings();
+ }
+
+ /**
+ * Starts the routine service for updating the preview programs.
+ */
+ public void startRoutineService() {
+ JobScheduler jobScheduler =
+ (JobScheduler) mContext.getSystemService(Context.JOB_SCHEDULER_SERVICE);
+ if (jobScheduler.getPendingJob(UPATE_PREVIEW_PROGRAMS_JOB_ID) != null) {
+ if (DEBUG) Log.d(TAG, "UPDATE_PREVIEW_JOB already exists");
+ return;
+ }
+ JobInfo job = new JobInfo.Builder(UPATE_PREVIEW_PROGRAMS_JOB_ID,
+ new ComponentName(mContext, ChannelPreviewUpdateService.class))
+ .setPeriodic(ROUTINE_INTERVAL_MS)
+ .setPersisted(true)
+ .build();
+ if (jobScheduler.schedule(job) < 0) {
+ Log.i(TAG, "JobScheduler failed to schedule the job");
+ }
+ }
+
+ /** Called when {@link ChannelPreviewUpdateService} is started. */
+ void onStartJob(JobService service, JobParameters params) {
+ if (DEBUG) Log.d(TAG, "onStartJob");
+ mJobService = service;
+ mJobParams = params;
+ updatePreviewDataForChannelsImmediately();
+ }
+
+ /**
+ * Updates the preview programs table.
+ */
+ public void updatePreviewDataForChannelsImmediately() {
+ if (!mRecommender.isReady()) {
+ mNeedUpdateAfterRecommenderReady = true;
+ return;
+ }
+
+ if (!mPreviewDataManager.isLoadFinished()) {
+ mPreviewDataManager.addListener(new PreviewDataManager.PreviewDataListener() {
+ @Override
+ public void onPreviewDataLoadFinished() {
+ mPreviewDataManager.removeListener(this);
+ updatePreviewDataForChannels();
+ }
+
+ @Override
+ public void onPreviewDataUpdateFinished() { }
+ });
+ return;
+ }
+ updatePreviewDataForChannels();
+ }
+
+ /** Called when {@link ChannelPreviewUpdateService} is stopped. */
+ void onStopJob() {
+ if (DEBUG) Log.d(TAG, "onStopJob");
+ mJobService = null;
+ mJobParams = null;
+ }
+
+ private void updatePreviewDataForChannels() {
+ new AsyncTask<Void, Void, Set<Program>>() {
+ @Override
+ protected Set<Program> doInBackground(Void... params) {
+ Set<Program> programs = new HashSet<>();
+ List<Channel> channels = new ArrayList<>(mRecommender.recommendChannels());
+ for (Channel channel : channels) {
+ if (channel.isPhysicalTunerChannel()) {
+ final Program program = Utils.getCurrentProgram(mContext, channel.getId());
+ if (program != null
+ && isChannelRecommendationApplicable(channel, program)) {
+ programs.add(program);
+ if (programs.size() >= RECOMMENDATION_COUNT) {
+ break;
+ }
+ }
+ }
+ }
+ return programs;
+ }
+
+ private boolean isChannelRecommendationApplicable(Channel channel, Program program) {
+ final long programDurationMs =
+ program.getEndTimeUtcMillis() - program.getStartTimeUtcMillis();
+ if (programDurationMs <= 0) {
+ return false;
+ }
+ if (TextUtils.isEmpty(program.getPosterArtUri())) {
+ return false;
+ }
+ if (mParentalControlSettings.isParentalControlsEnabled()
+ && (channel.isLocked()
+ || mParentalControlSettings.isRatingBlocked(
+ program.getContentRatings()))) {
+ return false;
+ }
+ long programLeftTimsMs = program.getEndTimeUtcMillis() - System.currentTimeMillis();
+ final int programProgress =
+ (programDurationMs <= 0)
+ ? -1
+ : 100 - (int) (programLeftTimsMs * 100 / programDurationMs);
+
+ // We recommend those programs that meet the condition only.
+ return programProgress < RECOMMENDATION_THRESHOLD_PROGRESS
+ || programLeftTimsMs > RECOMMENDATION_THRESHOLD_LEFT_TIME_MS;
+ }
+
+ @Override
+ protected void onPostExecute(Set<Program> programs) {
+ updatePreviewDataForChannelsInternal(programs);
+ }
+ }.execute();
+ }
+
+ private void updatePreviewDataForChannelsInternal(Set<Program> programs) {
+ long defaultPreviewChannelId = mPreviewDataManager.getPreviewChannelId(
+ PreviewDataManager.TYPE_DEFAULT_PREVIEW_CHANNEL);
+ if (defaultPreviewChannelId == PreviewDataManager.INVALID_PREVIEW_CHANNEL_ID) {
+ // Only create if there is enough programs
+ if (programs.size() > MIN_COUNT_TO_ADD_ROW) {
+ mPreviewDataManager.createDefaultPreviewChannel(
+ new PreviewDataManager.OnPreviewChannelCreationResultListener() {
+ @Override
+ public void onPreviewChannelCreationResult(
+ long createdPreviewChannelId) {
+ if (createdPreviewChannelId
+ != PreviewDataManager.INVALID_PREVIEW_CHANNEL_ID) {
+ TvContractCompat.requestChannelBrowsable(
+ mContext, createdPreviewChannelId);
+ updatePreviewProgramsForPreviewChannel(
+ createdPreviewChannelId,
+ generatePreviewProgramContentsFromPrograms(
+ createdPreviewChannelId, programs));
+ }
+ }
+ });
+ }
+ } else {
+ updatePreviewProgramsForPreviewChannel(defaultPreviewChannelId,
+ generatePreviewProgramContentsFromPrograms(defaultPreviewChannelId, programs));
+ }
+ }
+
+ private Set<PreviewProgramContent> generatePreviewProgramContentsFromPrograms(
+ long previewChannelId, Set<Program> programs) {
+ Set<PreviewProgramContent> result = new HashSet<>();
+ for (Program program : programs) {
+ PreviewProgramContent previewProgramContent =
+ PreviewProgramContent.createFromProgram(mContext, previewChannelId, program);
+ if (previewProgramContent != null) {
+ result.add(previewProgramContent);
+ }
+ }
+ return result;
+ }
+
+ private void updatePreviewProgramsForPreviewChannel(long previewChannelId,
+ Set<PreviewProgramContent> previewProgramContents) {
+ PreviewDataManager.PreviewDataListener previewDataListener
+ = new PreviewDataManager.PreviewDataListener() {
+ @Override
+ public void onPreviewDataLoadFinished() { }
+
+ @Override
+ public void onPreviewDataUpdateFinished() {
+ mPreviewDataManager.removeListener(this);
+ if (mJobService != null && mJobParams != null) {
+ if (DEBUG) Log.d(TAG, "UpdateAsyncTask.onPostExecute with JobService");
+ mJobService.jobFinished(mJobParams, false);
+ mJobService = null;
+ mJobParams = null;
+ } else {
+ if (DEBUG) Log.d(TAG, "UpdateAsyncTask.onPostExecute without JobService");
+ }
+ }
+ };
+ mPreviewDataManager.updatePreviewProgramsForChannel(
+ previewChannelId, previewProgramContents, previewDataListener);
+ }
+
+ /**
+ * Job to execute the update of preview programs.
+ */
+ public static class ChannelPreviewUpdateService extends JobService {
+ private ChannelPreviewUpdater mChannelPreviewUpdater;
+
+ @Override
+ public void onCreate() {
+ TvApplication.setCurrentRunningProcess(this, true);
+ if (DEBUG) Log.d(TAG, "ChannelPreviewUpdateService.onCreate");
+ mChannelPreviewUpdater = ChannelPreviewUpdater.getInstance(this);
+ }
+
+ @Override
+ public boolean onStartJob(JobParameters params) {
+ mChannelPreviewUpdater.onStartJob(this, params);
+ return true;
+ }
+
+ @Override
+ public boolean onStopJob(JobParameters params) {
+ mChannelPreviewUpdater.onStopJob();
+ return false;
+ }
+
+ @Override
+ public void onDestroy() {
+ if (DEBUG) Log.d(TAG, "ChannelPreviewUpdateService.onDestroy");
+ }
+ }
+}